Compare commits

..

164 commits

Author SHA1 Message Date
4645e4723b wiki publie : temoin a jour
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:49:55 -04:00
56f8d61ac6 wiki publie : temoin a jour
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:03:15 -04:00
aab1bcc83a doc : rangee par lecteur, trois portes d'entree (architecture, setup, usage) et un index
README reduit au role du projet et au demarrage rapide. docs/architecture.md (schemas
Mermaid), docs/setup.md, docs/usage.md (154 cibles en 13 familles), docs/README.md (index).
Documents ranges en conception/, exploitation/, hebergement/, projet/ ; 598 chemins suivis.
QUICKSTART.md, SOLUTION.md, architecture-set-ops.md et la carte fondus puis retires.

Preuves : P48 lit l'index et conception/mecanismes.md ; les gardes parcourent docs/ sous-
dossiers compris (_documents) ; usage.md suit le Makefile dans les deux sens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:01:54 -04:00
cdac6d0ea5 doc dns : l'interne et le public, deux sites, avec les schemas des processus et des flux
docs/dns-interne.md couvre desormais le DNS public sur deux sites : vue d'ensemble, chemin
d'une requete interne, publication d'une zone, requete de l'Internet, NS d'une zone (cinq
schemas Mermaid), et les tableaux des flux, des declarations, des gardes et de la bascule.
README de serveur_powerdns et de serveur_dns_public a jour ; le wiki y renvoie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:16:11 -04:00
16e87bfd41 CHANGELOG 152 : l'administration par le lien, appliquee et eprouvee de bout en bout
SSH et API de requin, maurice par le rebond, depuis le wifi par le tunnel admins.
Temoins : requin refuse depuis 172.31.0.2, maurice ne se joint pas sans le rebond.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:04:49 -04:00
e7d704fdcc inter-sites : l'administration du site pair, par le lien entre les sites
Une cle administration sur un lien de opnsense_liens_sites :
- entrante : les reseaux d'administration du pair (lus dans son plan) entrent par
  l'interface du lien, en SSH et HTTPS, vers la patte de gestion de la frontiere seulement ;
- sortante : la gestion et le tunnel des admins sortent vers le rebond du pair, et rien
  d'autre.
Routes derivees, gardees avec celles du lien. vpn_admin.py route le rebond de chaque pair.

frontiere-plan ne declare plus absentes du noyau les routes d'hote, que FreeBSD affiche
sans masque.

Sans la cle, le devis des deux sites est identique a l'ancien. Pas encore applique.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:36:19 -04:00
d152e32328 dns public : chaque zone designe aussi le serveur de noms du site pair
Le contrat du site porte dns_public_noms_pairs : nom et adresse publique du
serveur de chaque pair actif, lus dans son plan et sa carte de frontiere par
les memes fonctions que pour le site lui-meme. Le gabarit de zone ecrit un NS
par serveur ; la zone qui contient un de ces noms en porte le A, a l'adresse
de son site. Sans pair, le rendu est identique a l'ancien.

Garde au role (nom, adresse publique, jamais redeclare) ; la preuve du DNS
public refuse un pair actif sans dns_public_nom ; dns_bascule compte les
serveurs designes et sonde la colle de chacun.

Deploye chez les deux locataires : les deux serveurs publics servent
chezlepro.ca et technolibre.ca au meme serial, deux NS signes.

CHANGELOG 151.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:44:24 -04:00
ee5aad5f41 genome_pousser : la simulation dit ce que la poussee ferait, puis s'arrete
En --check, la sonde de l'organisation (une lecture uri) etait sautee et la
tache suivante echouait sur un statut absent, cache par no_log. La suite
(colis, reception, poussee) est faite de command et d'ecritures que --check
saute aussi : le verdict n'aurait rien prouve.

Les deux sondes de la forge tournent desormais en simulation. En --check, le
playbook lit la branche de la forge, la compare au poste (rev-list
--left-right), nomme pour chaque depot ce que la poussee ferait, et s'arrete
avant la premiere ecriture. Temoin : quatre cas nommes contre des clones
jetables ; a jour partout contre les vrais depots, aux deux sites.

CHANGELOG 150.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:11:52 -04:00
968caed3b1 inter-sites : le second serveur de noms, sur l'autre site, par le tunnel
- underlay.py : sites pairs lus dans leur depot frere (sites_pairs_actifs, sur_le_site,
  dns_public_des_pairs).
- Contrat du site : dns_public_pairs ; serveur_powerdns notifie et admet le pair ;
  resoudre_flux l'admet sur 5300.
- devis_opnsense : opnsense_liens_sites (regles et routes selon ou vit chaque locataire) ;
  P24 juge les routes du lien a part ; noms d'alias < 32 caracteres gardes ; le refus du
  groupe wireguard ne tranche plus (quick=0), et la garde le verifie.
- appliquer_opnsense : quick=0 transmis et dans l'identite d'une regle.
- proxmox_api : SETOPS_PROXMOX_API pour joindre l'API par un rebond.

Temoin depuis l'Internet : les deux zones repondent sur 51.79.45.193, signees.
95 preuves (make verifier), make test passe. CHANGELOG 149.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:26:52 -04:00
65a4268871 inter-sites : le lien entre les sites, et les pattes de tunnel hors des pattes de zone
- underlay.py : role d'hote frontiere_pair ; reseaux_tunnel() ecarte les tunnels des pattes
  de la frontiere (reseaux de gestion, SETOPS_FABRIC, SETOPS_FRONTIERE, resoudre_flux,
  supervision des pattes).
- devis_opnsense : seuls les roles des machines du plan font un alias (le relais en faisait un vide).
- La machine du cache recoit artefacts_amorcage_direct au lieu d'une valeur vide : le vide
  ouvrait la sortie 80/443 du socle a toutes les machines du site (jamais applique). P79
  revient a sa forme stricte.

95 preuves (make verifier), make test passe. CHANGELOG 148.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:38:40 -04:00
ca731eedc4 site : l'ordre d'un site neuf, le depot avant l'autorite et le cache avant tous
- Couche depot_etat juste apres le socle (serveur_backup, serveur_backup_site) : l'AC, les
  bases et la supervision trouvent un depot qui repond quand elles naissent.
- site-deployer-tout amorce d'abord la machine du cache, seule (socle, serveur_artefacts,
  serveur_cache_site) : aucune couche ne peut exprimer cet ordre par machine.
- verifier_flux_site.py refuse le deploiement si une machine active n'a pas ses regles
  nftables generees ; test_verifier_flux_site.py l'eprouve.

Simulation complete contre le site de TechnoLibre : aucun echec sur les neuf machines.
95 preuves (make verifier), make test passe. CHANGELOG 147.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:31:56 -04:00
a9428be752 genome_colis : la premiere reception clone depuis le colis
Un runner neuf face a une forge neuve n'avait aucun clone a avancer : recevoir() refusait.
Il clone desormais depuis le colis, puis suit le chemin ordinaire (verification, avance
rapide, poussee). Une destination existante qui n'est pas un depot reste un refus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:48:41 -04:00
810670cd30 site : le premier site construit depuis zero, et ce que le moteur supposait deja en place
- cloner_vm_debian / supprimer_vm_debian suivent SETOPS_UNDERLAY (le clonage visait le
  cluster du lien underlay.yml) ; sept cibles du Makefile aussi pour la voute.
- site_machines.py transmet au clone la cle, le domaine et le resolveur du plan du site.
- La machine qui porte le cache recoit artefacts_amorcage vide ; le socle retire alors le
  proxy herite du gabarit. P79 n'admet ce vide que sur la machine qui porte le cache.
- serveur_icingaweb2 : la base et le compte d'amorcage apres l'installation des paquets.
- genome_pousser : cle du runner, organisation genome et depots crees s'ils manquent.
- common_packages : la lecture apt-daily s'execute en simulation (check_mode: false).

95 preuves (make verifier), make test passe. CHANGELOG 146.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:46:46 -04:00
f9e45b1676 relais : la face publique d'un site sans adresse fixe, et le site de reprise de TechnoLibre
Le site de TechnoLibre n'a qu'une adresse DHCP. Trois adresses fixes sont louees sur un VPS
(le relais), qui repond a l'ARP pour elles et les route en /32 dans un tunnel WireGuard que la
frontiere initie. Aucune traduction : l'adresse du client arrive intacte.

- underlay.py : `tunnel: wireguard` dispense de VLAN ; role d'hote `relais`.
- devis_opnsense.py : `opnsense_face_publique`. Les entrees publiques s'y posent avec
  reply-to ; les sorties des locataires du pool passent par le relais ; une redirection n'y
  vise plus `wanip`. Le devis de Chezlepro est identique au caractere pres.
- appliquer_opnsense.py : replyto et gateway transmis et dans l'identite (seulement s'ils
  existent) ; le NAT sortant en mode automatic arrete l'application au lieu de dupliquer.
- role serveur_relais, inventorie depuis la carte comme un hyperviseur ; cle privee dans la
  voute du site. Deploye, idempotent.
- site_inventaire.py : les hyperviseurs ne sont plus filtres sur 192.168.11. ; site-appliquer
  charge la voute designee par SETOPS_UNDERLAY ; voute.py n'exige plus d'un locataire un
  secret du site.
- prouver.py : P67 lit le plan par underlay.plan_site() ; P53 couvre la face publique.
- Tests : test_underlay_tunnel, test_face_publique_relais, test_mode_nat_sortant.

95 preuves (make verifier), make test passe. CHANGELOG 144 et 145.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:20:04 -04:00
f3bb9caee3 chiffrement : metriques et journaux du site en TLS, et la frontiere certifiee par sa propre autorite
Metriques et journaux (CHANGELOG 142). Le site pose les quatre interrupteurs TLS
que chaque locataire pose dans ses group_vars : node_exporter et Loki servaient
en HTTP sur les neuf VM, le certificat etait la, inutilise. test_parite_tls_site.py
compare desormais le site aux locataires. Les hyperviseurs poussent leurs
journaux en HTTPS verifie, avec la racine du site comme simple fichier.
L'exportateur PostgreSQL sert en HTTPS partout, sur l'interrupteur de PostgreSQL
lui-meme, que Prometheus lit pour choisir son schema.

La frontiere (CHANGELOG 143). Une autorite subordonnee signee par la racine du
site, bornee (pathlen 0, noms sous le domaine et adresses du site, contrainte
critique), cle nee sur la boite (scripts/frontiere_pki.py). Syslog vers Loki en
TLS mutuel (seule la frontiere peut ecrire), exportateur en HTTPS, interface web
sur le certificat de services ; les clients d'API verifient la frontiere contre
la racine du site. La lecture du magasin passe par config.xml : l'API de
recherche renvoie les cles privees.

Deploye et mesure au site, chez les deux locataires et sur la frontiere ; 95 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:25:28 -04:00
78c3505577 courriel : la sonde boites cherche un vrai compte, et l'IMAP n'ecoute plus en clair
La sonde demandait `doveadm user`, que le userdb static (static_allow_all_users,
requis par la remise LMTP) satisfait pour n'importe quel nom : elle disait
« daniel » resolu chez deux locataires ou ce compte n'existe pas. Elle interroge
desormais le passdb (`doveadm auth lookup`), sans mot de passe, et cherche le
compte d'amorcage (amorcage_acces_courriel) plutot qu'un nom ecrit.

Le 143 (IMAP en clair) ecoutait par defaut a cote du 993 : eteint par
`inet_listener imap { port = 0 }`, eprouve sur banc Debian 13.

Deploye chez les deux locataires : 24 et 993 seulement ; sonde verte sur
sysadmin, rouge sur un compte inexistant. test_dovecot_sonde_et_imap.py garde
les deux ; 95 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:36:41 -04:00
63f5fda091 courriel : les mots de passe de soumission ne quittent plus la machine de Postfix
Postfix (edge-mta) faisait verifier chaque mot de passe de soumission par le
Dovecot du mailstore, sur :12345. Le client SASL Dovecot de Postfix ne chiffre
pas : chaque mot de passe traversait le reseau en clair, et le registre disait
ce flux tls.

Le flux est supprime, pas chiffre. serveur_postfix pose a cote de lui un
Dovecot d'authentification seule (serveur_dovecot/tasks/auth_locale.yml) :
coeur + LDAP, aucun protocole, aucun port, joint par private/auth, qui verifie
sur l'annuaire en LDAPS. Le 12345 disparait des deux roles et du pare-feu ;
serveur_dovecot_sasl_reseau est retire, et une garde arrete l'instance qui le
pose encore.

Deploye chez les deux locataires ; prouve par un envoi reel chez chacun
(sasl_username au journal, remise LMTP, relu par IMAP). test_sasl_local.py
garde le changement ; 95 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:26:18 -04:00
203f7c5436 courriel du site : chiffre de bout en bout, et le relais verifie qui il joint
La forge exige STARTTLS et verifie le certificat du relais. Le relais du site
exige le TLS en entree, verifie le serveur joint en sortie (autorites
publiques, lues avant le chroot), et journalise chaque session TLS.

Mesure sur un envoi reel : TLS 1.3 sur les deux troncons, remis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:30:50 -04:00
73fd941780 journal : aucun locataire ne porte de forge, seule celle du site etait touchee
L'entree 138 annoncait que les forges des locataires changeraient de relais.
Elles n'existent pas : seuls les sites portent une forge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:13:24 -04:00
92a396b3fe forge du site : ses courriels partent, par le relais du site
La forge nommait infra-mail-01 en dur comme relais : une machine de
locataire, absente du site. Elle derive maintenant son relais du groupe
serveur_postfix. Le relais du site admet les machines dont un role declare
un flux vers lui, lues dans le registre des flux (aujourd'hui la forge).

Deploye au site ; un courriel de mot de passe oublie est arrive et a servi.
Aussi : forge-comptes ne compte plus de changement en mode plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:57:43 -04:00
548d35b1b5 console : un mainteneur demande au site par un ticket, la console du site le voit
Lot 3b. make forge-comptes cree un compte par mainteneur declare (plan/mainteneurs.yml
du site), collaborateur en lecture sur les seuls depots de ses locataires, sans mot de
passe connu ; et le compte console-site en lecture seule, son jeton range sur le runner
du site hors de tout depot. La console du site met les demandes ouvertes dans A traiter ;
celle du locataire propose des demandes pre-remplies et le lien vers les siennes.

95 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:47:06 -04:00
6df04888ab journal : Technolibre reconstruit avec des depots prives, chaque runner ne lit que le sien
Bascule par make forge-cles, mesuree ETANCHE. Reconstruction complete de Technolibre :
depot apporte en colis a l'insemination, tire en SSH apres l'armement, flotte remontee,
temoins d'etat intacts sur les 9 machines qui en portent. 36 minutes depuis la reprise.

95 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:27:49 -04:00
3fc16c845d serveur_ops : le dossier SSH avant d'y epingler la forge
La reconstruction de Technolibre s'est arretee a l'insemination : la cle d'hote de la
forge s'ecrivait dans /opt/setops/.ssh, que le role ne creait que plus loin. Les runners
existants l'avaient deja ; seule une machine neuve le montre.

95 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:18:52 -04:00
82a2c4a4c2 forge : le colis se recoit hors d'un depot, et forge-cles reconcilie la visibilite
L'epreuve du colis sur le runner de Technolibre : git bundle verify exige un depot
existant, et l'insemination est le cas ou il n'existe pas encore. Le clone verifie le
colis lui-meme ; un depot existant le verifie avant de tirer. Les colis restes d'un
passage interrompu sont retires.

make forge-cles rend aussi chaque depot prive ou public selon le role que lui donne le
plan du site, et seulement une fois les cles de lecture en place.

95 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:58:08 -04:00
d7717a4f98 serveur_ops : les depots prives se lisent en SSH, ou arrivent en colis
Mesure sur le terrain : chaque runner lit son depot en SSH avec sa cle de deploiement,
et la forge lui refuse les autres, alors meme que les depots sont publics. Le role ne
clone plus en HTTPS que les depots publics ; les depots instance, tenant et hebergeur
passent en SSH, cle d'hote epinglee, commande SSH posee dans leur config git. Avant
l'armement, le controleur les apporte en git bundle. La cle d'hote de la forge est
declaree au plan du site et offerte par la fiche. genome_etat et genome_pousser lisent
l'API authentifies ; la sonde genome-servi compte les publics ; forge_amorcer cree
prives les depots des ecosystemes et du site. make forge-etancheite mesure le tout.

Les depots restent publics.

95 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:33:38 -04:00
9af39b1b60 forge : chaque locataire publie la cle qui lira son depot, le site sait l'enregistrer
Debut du chantier des depots prives (docs/depots-prives-conception.md). La face reseau
publie genome: {depot, cle_lecture} ; P95 exige que la cle soit connue de la flotte, et
l'armement refuse une voute qui en remettrait une autre. make forge-cles enregistre
chaque cle en lecture seule sur le seul depot que la face nomme, retire une cle
perimee, ne touche jamais celle du runner du site. Flux SSH vers la forge du site
depuis les seuls runners des locataires (paire runners_locataires).

Rien n'est encore applique sur le terrain ; les depots restent publics.

95 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:59:07 -04:00
aa5e4e0897 console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.

94 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
0d5ec924cc materialisation : le site pose lui-meme la cle de son runner en clonant
La cle du runner du site transitait par la face du locataire : lue dans le plan du
site, publiee dans la face, relue par le site. Le runner d'un locataire, sans ce
plan, recalculait une face sans elle. La face ne la porte plus ; le site l'ajoute en
clonant la machine qui porte serveur_ops_tenant, et elle seule, en tete des cles
(clonage_rendu). Le clonage par la face reste identique a l'octet pres au chemin du
poste (P93 le compare cle comprise). La console juge de nouveau la face partout.

94 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:43:58 -04:00
1d582045f3 console : la face ne se juge que la ou la carte du site est montee
Sur le runner de Technolibre, la vue Lien avec le site disait la face a republier :
la cle d'amorcage d'ops-01 vient du plan du site, absent du runner, et la face
recalculee la perdait. Republier d'ici l'aurait retiree. La face se lit partout et
ne se juge plus que la ou le site est monte, comme la fiche. Un test le garde.

94 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:22:31 -04:00
9592309f36 console : chaque bout du contrat site-locataire se lit dans sa console
Deux routes en lecture, /api/lien-site et /api/locataires-heberges, et deux vues
sous Veille : le locataire voit sa face reseau publiee et la fiche que son site lui
a deposee, le site voit ce que chaque locataire lui a publie. Chaque fichier est
montre avec son commit, que l'autre bout lit.

Un site de reprise ne juge plus la fiche deposee par l'hebergeur actif : il la
declarait perimee a chaque lecture (SITE-Technolibre). Un test le garde.

94 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:31:13 -04:00
493a5dcbeb console : une navigation par portee, des tableaux, les gestes en tete de fiche
Lot 1 de la refonte (affichage seul, aucune route ni pouvoir touche) :
- en-tete d identite colore par portee, PROD avec le nom ;
- navigation propre au locataire et au site, encadre Cette console peut ;
- parc en tableau groupe par categorie ; noeuds de la fabric et machines du site ;
- Verifier puis Deployer en tete de fiche ; assistants hors portee replies ;
- bandeau Plan modifie a la place du point sur le bouton.
Rapport de preuves du jour versionne, carte a 53 pieces d audit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:43:01 -04:00
412133bd76 runbooks : le site cree une seule machine de locataire, d apres sa face reseau
L assistant Tenir un site en etat ne proposait que locataire-creer, qui clone toutes
les VM manquantes. Il porte desormais l etape creer-vm (TENANT, HOTE), qui lit les
parametres de clonage de la face reseau publiee. Sans TENANT, la cible refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:12:53 -04:00
ee0e33d452 flux : plus aucune sortie vers Internet sans un role qui le dise
La sortie 80/443 que le socle donnait a toute machine ne servait plus a apt
(tout passe par le cache du site) mais a de la telemetrie et a des sorties
non declarees. Elle devient conditionnelle (seulement_si artefacts_amorcage
vide) ; la telemetrie de Grafana, Loki, Alloy et la verification de version
de Collabora sont coupees a la source ; rspamd et la sonde du genome
declarent leur sortie. Le devis du site evalue enfin seulement_si, ce qui
retire aussi le 465/587 vers un Postfix du site qui n'ecoute que sur 25.
Garde : test_conditions_site.py. Journal (125).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:11:13 -04:00
dd4ca09245 pare-feu : conserver et réconcilier les raisons des flux 2026-10-09 19:56:28 -04:00
162604ad24 journal : les deux locataires reconstruits sur 34e961f, sans PyPI ni Galaxy
(122) Technolibre : trois passages, le dernier de bout en bout en 36 min ;
runner monte depuis le site seulement, pip freeze egal au verrou.
(123) Chezlepro : 43 min d'une traite, memes constats.

94 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:22:29 -04:00
34e961fb61 serveur_ops : la liste hors ligne des collections porte les vraies versions
Le requirements.yml genere portait version: \1 (regex_replace dans un bloc
YAML) ; ansible-galaxy refusait, et la reprise de la reconstruction de
Technolibre s'est arretee apres avoir pris roues et collections au site.
Version tiree du nom par decoupage, sans expression reguliere ; rejoue sur
le poste, les trois collections s'installent hors ligne.
test_verrou_runner.py rend desormais le fichier avec Jinja2 et compare.

94 preuves. CHANGELOG (121).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:35:18 -04:00
422b68846b serveur_ops : creer les depots de roues et de collections avant la copie
La copie fichier par fichier du verrou ne cree pas son dossier, contrairement
a l'ancienne copie du cache entier. La reconstruction de Technolibre s'est
arretee a l'insemination sur un runner neuf (roues bien prises au site).
test_verrou_runner.py exige desormais que chaque dossier vise soit cree avant.

CHANGELOG (121) : l'arret, et le blocage site-pki-01 -> 10.37.32.1:8006 vu a
la frontiere, qui est la sonde de connectivite (resultat attendu).

94 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:22:01 -04:00
55f42add15 serveur_ops : ne pas accuser le depot du site en simulation
En --check, rien n'est telecharge depuis le site : le cache du controleur
reste vide et les messages annoncaient les 15 roues et les 3 collections
absentes du depot. Ils se taisent desormais en simulation.

CHANGELOG (121) : serveur_artefacts deploye au site, 18 sommes servies
conformes depuis ops-01, serveur_ops simule sur ce runner.

94 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:35:23 -04:00
4ebca13058 runner : roues et collections prises au site, plus a PyPI ni a Galaxy
verrou-runner.yml fige les 15 roues et les 3 collections qui tournent sur le
runner de Technolibre, avec URL amont et somme SHA-256. Le cache du runner
tenait deux versions de six bibliotheques selon le jour du montage.

- serveur_artefacts depose ces 18 fichiers au depot de binaires du site.
- serveur_ops les prend au site d'abord, somme verifiee ; repli sur Internet
  dit en clair ; plus aucun pip download ni collection download ; seuls les
  fichiers du verrou sont deposes sur le runner.
- ansible-core epingle a 2.18.19 (la version deployee).
- P70 exige le verrou au site ; test_verrou_runner.py dans make test.

AGENTS.md et CLAUDE.md : version allegee par l'operateur.

94 preuves. CHANGELOG (121).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:24:38 -04:00
279e583701 journal : les deux locataires reconstruits sur 3afb756, sans l'app store
Technolibre (43 min) et Chezlepro (44 min), d'une traite : 10 jeux restaures, temoins sans ecart, minuteurs apt remis a l'heure sur 12 VM, applications Nextcloud prises au site. Code a etiqueter : v2026.10.09.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:01:22 -04:00
3afb756993 common_packages : pas de rattrapage des minuteurs apt herite du gabarit
Les minuteurs apt persistants d'un clone portaient la trace du gabarit (2026-06-21) et rattrapaient un passage dans l'heure suivant la naissance : apt-daily-upgrade a pris le verrou du cache des archives en plein amorcage d'infra-pki-01. Une trace plus ancienne que la machine est remise a l'heure et le minuteur relance, avant toute operation apt. Les correctifs restent automatiques.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 01:49:37 -04:00
c7378ed7f7 nextcloud : les applications viennent du depot du site, plus de l'app store
user_oidc et richdocuments epinglees (version, sha256), tenues par site-cache-01 dans le depot de binaires, installees par app_tierce.yml : site d'abord, amont en repli dit, somme puis signature (occ integrity:check-app), activation. P70 comprend les listes d'artefacts. L'app store a 77 ko/s avait arrete la reconstruction de Technolibre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 22:47:33 -04:00
7a4c55bfc9 journal : sonde corrigee deployee, sauvegardes du site OK ; une fausse alerte sur le mot de passe d'API
client_backup (Technolibre, site) et serveur_backup (site) deployes ; les six services de sauvegarde du site OK apres relance. La difference de mot de passe vue en simulation n'etait qu'un blanc de fin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:27:55 -04:00
0f7c89fbb5 sondes de sauvegarde : juger le plus recent instantane ; temoin Loki hors index
restic snapshots --latest 1 rend un instantane par groupe (machine et chemins) : la sonde gardait le premier, l'ancien, depuis qu'un jeu a ete ajoute a site-mon-01 (EN RETARD a tort, PERIME promis a 50 h). Meme motif cote serveur_backup. Le temoin de Loki ne compte plus l'index compacte. Journal (114), (115).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:35:28 -04:00
9ca9a5ffe1 valider : la recette Prometheus attend une minute au plus que ses cibles soient relevees
La reconstruction de Technolibre s'est arretee : la recette a juge Prometheus 18 s apres sa relance par le premier depot d'obs-01. Elle redemande desormais, puis juge. Journal (112) Keycloak et (113).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:20:45 -04:00
2dce6eabd5 journal : catalogue des capteurs au site ; icingadb-redis mis a jour sans que la simulation le montre
serveur_icinga deploye au site : catalogue pose, epinglage des paquets tiers. icingadb-redis 8.2.9 -> 8.2.10 (installe depuis le cache, invisible en --check) ; Icinga 2 et Icinga DB reconnectes. VM 99998 supprimee par l'exploitant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:16:00 -04:00
479ec3cc7d journal : l'arret de Loki attend un envoi d'Alloy (10 s), pas la copie
Correction de (109) : la liaison des morceaux fonctionne (copie ~60 ms) ; les 10,75 s viennent de l'arret propre de Loki, qui attend un envoi d'Alloy jusqu'a son delai de 10 s. Aucune perte mesuree ; laisse tel quel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:54:14 -04:00
9a4b63544e loki : les morceaux sont lies, pas copies ; chunks/index reste copie
Copier 6 550 morceaux arretait Loki 13 s au site ; les lier prend 40 ms. Ils sont adresses par leur contenu ; chunks/index loge delete_requests.gz, qui se reecrit, et reste vraiment copie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:44:04 -04:00
3e2846cc5f journal : le site recoit l'outillage de restauration et la copie a froid de Prometheus et Loki
Les machines sauvegardees du site n'avaient pas setops-restaurer. client_backup deploye au site : 0 echec ; Prometheus arrete 523 ms, un releve manque ; Loki 13 s, aucun journal perdu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:34:24 -04:00
cca22f47ca journal : Prometheus et Loki deployes chez les deux locataires, un seul releve manque
Frontiere sans changement ; pare-feu Proxmox et nftables de mon-01 ouverts a obs-01 ; premier depot : Prometheus arrete 85 a 151 ms, Loki 582 a 614 ms, ecart maximal 30 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:14:52 -04:00
4660a8892a prometheus et loki : copie a froid, restauration et temoins
Les metriques et les journaux repartaient de zero a chaque reconstruction. setops-copie-a-froid arrete le service le temps de copier et le relance quoi qu il arrive ; les roles remettent la copie avant de demarrer ; les temoins jugent la couverture, pas les fichiers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:57:04 -04:00
6141bc4b2f journal : le gabarit 99998 sort des declarations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:17:00 -04:00
e8907285d5 publier : les releases voyagent jusqu'aux deux forges ; wiki publie sur la forge du site
Le colis du genome ne portait que la branche : les etiquettes de release
n'arrivaient jamais sur la forge du site. Il les porte desormais, le runner les
pousse sans forcer, et la forge est relue par son API. publier.py pousse aussi
les etiquettes vers eregion. Le wiki, jamais publie sur la forge du site, l'est.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:04:31 -04:00
e9215a2141 journal : les deux locataires reconstruits sur 2cbfa2d, l'historique d'Icinga traverse
Technolibre puis Chezlepro, 43 min chacun, temoins sans ecart. L'AC d'Icinga
restauree garde l'environnement ; l'historique d'avant reste, 0 ligne orpheline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:12:05 -04:00
2cbfa2df9b journal : client_backup et serveur_icinga deployes, les temoins par cle voient l'historique perdu
Sur les deux locataires : l'AC d'Icinga est desormais sauvegardee. Rejoues sur
Chezlepro, les temoins par cle voient les 412 lignes d'historique perdues que le
comptage cachait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:35:35 -04:00
f04e790d2c icinga : l'AC et l'historique survivent a la reconstruction ; temoins par cle
L'historique d'Icinga DB etait exclu de la restauration (2026-09-30) : son
environnement derive de l'AC d'Icinga, recreee a chaque reconstruction. L'AC est
desormais un jeu de sauvegarde, remis avant api setup ; la base d'Icinga est
restauree, et son schema n'est importe que sur une base vierge.

Les temoins comparaient PostgreSQL par nombre de lignes et n'ont pas vu 412
lignes d'historique remplacees par 380 neuves. Ils comparent desormais par cle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:18:40 -04:00
12caff1cff devis de placement : le gabarit et le stockage que le clonage utilise (ceux du site)
Le devis lisait le gabarit du tenant : il validait chez Technolibre un 99998 que
plus rien ne clone, et refusait Chezlepro, qui suit la regle et n'en declare plus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:24:39 -04:00
543791e859 temoins : toutes les bases, modeles compris (template1 dite perdue a tort)
Premier essai reel sur Technolibre : pg_dumpall --clean recree template1, et
les bases vivantes etaient lues sans les modeles. Le test reproduit le filtre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:55:53 -04:00
11b5bb5733 temoins : l'instantane d'avant rasage est garde, et l'etat remis lui est compare
M4 a reconstruit Technolibre par le site qui la nomme ; les 7 jeux sont revenus
de l'instantane de 11:34, mais le premier depot d'apres reconstruction l'a chasse
par --keep-daily le jour meme : plus rien a quoi comparer.

Le depot d'avant rasage est etiquete avant-raser et garde jusqu'a la
reconstruction suivante. Le bilan dit ce qui a ete restaure et si c'est encore
au depot. setops-restaurer temoins et make temoins-etat comparent l'etat vivant
a cet instantane : une perte ou une identite changee est un ecart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:48:00 -04:00
a204ead8ab pre-vol M4 : raser et le devis de placement nomment le locataire designe
Sur le chemin TENANT=, raser annoncait un ecosysteme monte (rien n'est
monte sur le runner du site) et le devis de placement un tenant « ? ».
Les deux en-tetes disent desormais ce qui est vise et d'ou vient la liste.
Le pre-vol lui-meme est consigne : 13 VM, aucun conflit, placement conforme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:16:04 -04:00
e7f1a74040 contexte : materialisation M3, la reconstruction nomme son locataire
reconstruire_locataire lance locataire-raser et locataire-creer TENANT= sur
le runner du site, sans monter le depot du locataire comme instance.

Trou de M2 corrige avant tout usage : le sous-make cloner-vm nommait le pool
par l'instance montee (hors de tout pool sur le site, pool de l'autre
locataire sur le poste). devis_proxmox_pools.py --pool-du-locataire le
derive de la face.

test_appels_locataire.py : un faux ansible-playbook consigne la ligne qui
part ; 26 machines, ancien chemin, site et poste identiques a l'argument
pres. Temoin : l'ancienne ligne de pool fait echouer le test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:27:25 -04:00
8294e5dc71 contexte : materialisation M2, les commandes du site nomment leur locataire
locataire-creer, locataire-raser et placement-plan TENANT= lisent la face
reseau du locataire, sans monter son depot comme instance. flotte-creer,
creer-vm et raser acceptent TENANT= ; sans lui, l'ancien chemin est inchange.

P94 : creer, raser et placer par la face visent les memes machines, VMID,
valeurs et ponts que l'ancien chemin, chacun mesure par sa commande lancee
a part. 116 controles dans test_contexte.py, temoins compris.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:08:21 -04:00
4532a98707 contexte : materialisation M1, la face publie les parametres de clonage (P93)
Chaque machine porte ce que parametres-proxmox rend, par la meme fonction.
P93 compare a la commande lancee a part pour les 26 machines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:22:43 -04:00
40af2d1ec6 contexte : etape 3, les pools et les tunnels lisent la face reseau
devis_proxmox_pools et vpn_admin prennent machines, VMID, etat, pairs et
index dans la face publiee. Sorties identiques a un worktree complet de
la version precedente ; temoins d'alteration verts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:59:29 -04:00
2e9b31bef7 contexte : corriger le devis des pools vide (face sans fonctions)
La face publie aussi fonctions ; les pools retrouvent leurs VM. La
comparaison a l'ancien code est refaite dans un git worktree complet :
les sept sorties du site sont identiques. Garde de regression ajoutee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:49:24 -04:00
d0a6002b87 contexte : etape 3, le site decouvre ses locataires par leur face reseau
decouvrir_du_site part de underlay.tenants et prend index et zones dans
la face publiee. Les sept sorties du site sont identiques a HEAD, a
l'octet pres ; frontiere-plan ne voit rien a faire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:10:39 -04:00
013c1b5279 contexte : etape 3, la frontiere lit la face reseau
devis_opnsense prend dans la face publiee les adresses des groupes,
l'intrant d'administration, les verdicts, le tunnel et les zones
publiees. Devis identique a l'octet pres, a HEAD comme par le repli.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:02:03 -04:00
8c1e8c725b contexte : etape 3, le pare-feu Proxmox lit la face reseau
La face publie le verdict des flux conditionnels ; devis_proxmox_fw y lit
inventaire, verdicts et administration. Devis identique, octet pour octet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:51:37 -04:00
44c24e62e4 contexte : etape 3, le locataire publie sa face reseau, le site la lit (P92)
face-reseau.yml publiee chez chaque locataire (make face-reseau-publier) ;
les comptes de sauvegarde et le DNS public du site la lisent au lieu des
fichiers internes. Inventaire du site identique, octet pour octet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:39:28 -04:00
2beef04dc6 contexte : etape 3, l'instancier lit la fiche du site (P91)
Le site depose fiche-site.yml chez ses locataires (make fiches-site-
deposer) ; l'instancier y lit temps, delegation DNS et routage. Sans le
site, l'inventaire genere est identique au versionne, a l'octet pres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:23:17 -04:00
d6591cb271 contexte : etape 2 terminee, les sorties (P90)
resoudre_flux publie les sorties vers l'Internet de chaque machine ;
verifier_sorties les confronte aux regles de sortie de la frontiere.
Le contrat site <-> locataire est entierement publie et prouve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:21:37 -04:00
92a6fb0ead nftables_baseline : la passerelle n'est pas une porte (garde 8006)
La sonde connectivite essaie l'API Proxmox (8006) sur la passerelle par
defaut : chez un locataire en SDN, c'est l'hyperviseur dans son VRF, hors
de la frontiere. Ouvert -> CRITIQUE. Temoin : CONNECTIVITE_PASSERELLE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:12:37 -04:00
ce84d0e1ea frontiere : l'administration (P89) ; serveur_ops ne declare plus le 8006
La face reseau dit quelles entrees publiques sont ouvertes au poste ;
verifier_administration confronte gestion, VPN et tunnel. La sortie 8006
de serveur_ops vers externe ne laissait passer aucun paquet (hyperviseurs
en plages privees) et affirmait un pouvoir qu aucun locataire n a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:14:14 -04:00
56033afd00 serveur_ops : retirer la sortie 443 vers voisins_site (passage inter-locataires)
Ecrite quand la forge du genome vivait chez un voisin ; elle est au site.
Vue d un locataire, voisins_site designe les autres locataires : la
frontiere ouvrait le 443 de chaque runner vers le supernet de l autre.
Le clonage passe par la regle de serveur_forge_site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:38:18 -04:00
3c16ef45cd contexte : etape 2, la frontiere second temps, les entrees publiques (P88)
La face reseau porte le port de son tunnel ; verifier_entrees_publiques
confronte regles WAN, redirections et tunnel aux entrees que la face
ouvre a tous. Aucun ecart ; quatre alterations vues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:14:32 -04:00
78c2c25761 contexte : etape 2, la frontiere premier temps, les identites (P87)
La face reseau porte ses zones ; verifier_frontiere confronte supernet,
administration, tunnel, alias de groupe, routes et traduction sortante a
la face reseau et a la fiche du site. Aucun ecart ; cinq alterations vues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:28:06 -04:00
3d01208a68 contexte : etape 2, les flux de chaque machine (P86)
La face reseau porte les flux resolus par le locataire ; verifier_flux
les confronte au devis Proxmox, port par port. resoudre_flux publie les
sources_declarees d un port aussi public, que le locataire jetait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:10:12 -04:00
077c6c91b4 contexte : etape 2b, la face reseau du locataire (les faits)
locataire.face_reseau() publie index, machines, groupes, administration,
zones publiques et cle de sauvegarde ; verifier_face la confronte aux
consommateurs du site (decouverte, devis Proxmox, frontiere, inventaire
du site). P85 : aucun ecart ; huit alterations vues en ecart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:02:29 -04:00
8251022148 contexte : etape 2a, la fiche du site et la preuve qu elle dit vrai
site.fiche_pour(locataire) reunit attribution, offre, racine et les
trois lectures directes de l instancier ; verifier_fiche la confronte
aux copies, a la racine et a l inventaire du locataire. P84 : aucune
ecart sur les deux couples ; sept alterations vues en ecart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:55 -04:00
c891d7fb26 contexte : etape 1, le tronc commun et les deux classes (inutilises)
scripts/contexte.py : Ecosysteme, Site, Locataire, contexte actif
(fichier `contexte`, sinon les indices d avant via instance_courante
et underlay.chemin). 40 controles dans make test. Corrige aussi P34 et
P48, rouges depuis le commit de la page de conception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:43:20 -04:00
42e23885cd conception : un tronc commun, deux classes (SITE et LOCATAIRE)
docs/conception-contextes.md, arretee avec l exploitant : le releve
des devinettes de contexte et des echanges site <-> locataire, le
contrat en deux fiches, le poste selecteur, le chemin en neuf etapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:26:32 -04:00
0d7578b367 journal : release v2026.10.04, les deux locataires reconstruits sur f42d30b
Technolibre et Chezlepro, 42 min chacun, sans arret ni echec ; (73),
(74) et les corrections de (76) relus sur les machines neuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:12:31 -04:00
f42d30b38a validation avant release : test casse par (74), trois defauts d outillage
test_restauration fournit client_backup_attente_verrou et exige
--retry-lock ; test_frontiere_refus lit le WAN sous if_wan ; prouver
--verifier nomme les preuves en echec ; le Makefile n exporte plus une
liste de cles de voute vide (valeur identique sur le poste).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:46:43 -04:00
5d7d2a8918 exporter : archives datees, et rien d ecrit si les voutes n ont pas change
Le nom par defaut etait fixe : le second export butait sur le premier.
Il porte maintenant la date (l heure en plus le meme jour) ; un export
de voutes identiques a la derniere archive n ecrit rien. Le LISEZ-MOI
et la doc restaurent la plus recente archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:37:07 -04:00
1167cc12e0 client_backup : un depot verrouille n est plus une panne
Les quatre scripts qui touchent au depot appellent restic avec
--retry-lock (client_backup_attente_verrou, 30 min) : deux minuteurs
partis ensemble ne fabriquent plus de faux critique (aucun instantane,
depot corrompu). Eprouve sur un depot jetable, dans les deux sens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:27:11 -04:00
7976df08d5 client_backup : un instantane vide dit pourquoi
Devant un instantane vide, les sondes sauvegarde et restauration comptent
ce que les chemins sauvegardes contiennent sur le noeud : rien (RIEN A
SAUVEGARDER, avertissement), arrive apres l instantane (avertissement),
ou deja la a sa prise (critique : la sauvegarde manque ses donnees).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:06:31 -04:00
6570b757a4 journal : les deux locataires reconstruits, les correctifs tiennent
Technolibre en 44 min, Chezlepro en 42 min, sans arret ni failed ;
7 jeux d etat restaures chez chacun ; versions, epingles, vigie, vues
metier, filtres Alloy et synthese relus sur les machines neuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:01:04 -04:00
5340220192 vigie : vues métier par clientèle interne, dérivées du plan
- metier: sur chaque sonde (47) ; catalogue des vues et services dans
  serveur_icingaweb2 ; vues Mes outils / Services rendus / Exploitation
- contrôles calculés comme serveur_icinga (sauvegardes, socle, matériel,
  frontière) ; vue vide non posée ; ancien exemple supervision retiré
- CHANGELOG 71

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:26:11 -04:00
055aed665e grafana : la synthèse comme page d'accueil
- setops.conf : GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH vers setops-synthese.json
- CHANGELOG 70 complété

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:43:33 -04:00
05ec7a8743 grafana : un tableau de synthèse, au site et chez les locataires
- tableau-synthese : faut-il agir, marge, activité, où regarder ensuite ;
  hyperviseurs au site seulement ; chaque panneau dit quoi faire
- CHANGELOG 70

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:12:00 -04:00
b3debd6ca9 grafana : le tableau Matériel dit quoi faire
- materiel.py : textes accentués et champ action par mesure (seuils inchangés)
- tableau-materiel : mode d'emploi en tête ; descriptions en trois temps
  (ce que ça mesure, seuils, quoi faire)
- CHANGELOG 69

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 00:49:28 -04:00
49f3d28865 forge et nuage : Forgejo 16.0.5, Nextcloud 34.0.4 ; Nextcloud sait monter
- serveur_nextcloud : tasks/monter.yml (maintenance, ancien code mis de côté,
  data/config/ajouts repris, occ upgrade) ; signature PGP vérifiée
- serveur_forgejo : 16.0.5 ; une montée se simule
- serveur_artefacts : le site sert aussi la signature de Nextcloud
- CHANGELOG 68

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 00:09:03 -04:00
16675037f5 identité : Keycloak 26.7.5, oauth2-proxy v7.15.5 ; les rôles savent monter
- serveur_keycloak : une montée remplace la distribution (arrêt, retrait,
  extraction) au lieu de construire les anciens fichiers sous le nouveau repère
- serveur_oauth2_proxy : repère = version du binaire ; empreinte SHA-256
  épinglée, archive refusée et retirée du cache si elle diffère
- CHANGELOG 67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 23:32:10 -04:00
715f9de84d paquets tiers : les épingles tiennent, cache ou pas
- épingles posées en préférences apt (990) par chaque rôle et par le socle
  avant son dist-upgrade ; garde : paquet plus récent ou épingle inopérante
- quatre épingles relevées (step-cli, icingadb-redis, icinga-php-*)
- paquets_tiers : --force-confold aux mises à jour ; une mise à jour compte
  comme un changement
- CHANGELOG 66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:52:25 -04:00
badaf753f8 paquets tiers : épingles Grafana relevées sur ce que portent les locataires
- alloy 1.20.1-1, grafana 13.2.3, loki 3.7.8 (les runners avaient tiré
  la dernière version publiée ; le cache du poste aurait rétrogradé)
- CHANGELOG 65

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:33:31 -04:00
4ee149a0c6 vigie : psycopg2 sur l'hôte de la vigie ; base appliquée chez les locataires
- serveur_icingaweb2 : python3-psycopg2 avant la première requête vers sa base ;
  au site il venait de PostgreSQL, co-localisé, chez un locataire il manquait
- CHANGELOG 64 : déploiement relu chez Chezlepro et Technolibre

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:17:38 -04:00
f11bb11ad7 vigie : sa base dans tous les modes ; trois restes du site corrigés
- serveur_icingaweb2 : base de console résolue et utilisée en SSO comme en db
  (config_backend db) ; le badge de migrations la réclame sans condition
- serveur_icingaweb2 : php8.4-fpm et nginx avant les paquets tiers ; apache2
  retiré s'il part seul (apt-get -s purge)
- serveur_nginx : ssl_protocols de Debian neutralisé (doublon à chaque reload)
- site_inventaire : les hyperviseurs ne reçoivent plus le cache du site ni ses
  amorçages, qu'ils n'atteignent pas
- exemple de voûte : vault_bd_icingaweb2 dans tous les modes ; CHANGELOG 64

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:04:51 -04:00
2f275d8a7d make : appliquer refuse un GROUPE non donné ; locataires corrigés et mesurés
- appliquer, deployer-groupe et site-appliquer refusent quand GROUPE vient
  du défaut du Makefile ($(origin GROUPE) = file) ; l'ancienne garde -z ne
  pouvait jamais se déclencher, et un make appliquer seul avait appliqué
  serveur_debian à tout Technolibre
- CHANGELOG 63 : déploiement chez les deux locataires relu sur 26 machines ;
  journal -77 % / -86 %, audit -72 % / -80 %, edge-mta-01 -96 % / -98 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:08:21 -04:00
b4ae92c03f journaux : le bruit de sonde du courriel filtré ; make appliquer accepte ARGS
- client_journal : sept stage.drop de plus pour Postfix (connect, lost
  connection after CONNECT, disconnect commands=0/0, SSL_accept error) et
  Dovecot LMTP ; phrase exacte, seulement depuis une machine sondeuse
- éprouvé sur une heure réelle de Chezlepro (69 % jeté, 96 % sur
  edge-mta-01, le scanner externe reste visible) et avec le binaire Alloy
  1.20.1 (9 lignes de sonde jetées, 7 légitimes reçues)
- Makefile : appliquer passe ARGS, comme site-appliquer
- CHANGELOG 63 : mesure des locataires ; cache de paquets du poste plus
  vieux qu'eux (alloy 1.19.2 / loki 3.7.7 contre 1.20.1 / 3.7.8)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:01:07 -04:00
8d40c687e1 site : journaux allégés, porteur de santé retiré des hyperviseurs
- client_sante : tasks/retirer.yml, joué sur hyperviseurs:!client_sante ;
  le minuteur resté depuis le 2026-09-10 visait 10.0.36.11 et échouait
  toutes les 15 min (une unité en échec permanente par hyperviseur)
- site_inventaire : plus de client_sante_icinga_url pour les hyperviseurs
- client_journal : loki.process jette le bruit de la sonde connectivite,
  phrase exacte et seulement depuis une machine de client_sante
- serveur_loki : log_level warn (Loki réingérait ses propres requêtes)
- auditd : règle never pour adjtimex de node_exporter (35 % de l'audit) ;
  nouveau handler augenrules --load, un restart d'auditd ne rechargeait
  pas les règles sur Debian 13
- audit : rapport de preuves du 2026-10-03, carte à 51 pièces

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:58:47 -04:00
8b008a8cf4 nextcloud : le groupe d'habilitation redevient admin apres une restauration
L'etape tournait avant la remise du Nextcloud restaure, sur une base ou le
groupe n'existait pas encore : la base d'avant revenait avec son etat d'avant.
sysadmin etait admin chez Technolibre, pas chez Chezlepro. Sortie dans
admin.yml et rejouee apres la remise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:26:03 -04:00
9414dca750 journal : la frontiere refuse a voix haute a l'interieur
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:10:40 -04:00
6a8fc53084 frontiere : les interfaces internes refusent a voix haute, le WAN reste muet
Un reject final, journalise, sur chaque interface interne (zones du site,
gestion, transit, WireGuard) : un flux mal declare entre deux zones echoue
au lieu d'expirer. P53 affinee, P50 ne compte que les block, test du devis.
La sonde classe ECONNREFUSED en avertissement : le RST de la frontiere est
indiscernable d'un port ferme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:39:46 -04:00
a4b6565056 journal : Chezlepro reconstruite en 41 minutes, sans arret
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:49:03 -04:00
7a2f179f39 repertoires partages : un seul mode par repertoire ; le depot de binaires du site sert de nouveau
/var/lib/setops etait tenu par quatre roles (0750 contre 0755) : apt-cacher-ng
ne le traversait plus, le depot rendait 403 et chaque runner reconstruit allait
chercher 500 Mo sur Internet. /etc/setops (0700 contre 0755) et /srv/restic au
site portaient le meme desaccord. test_repertoires_partages.py le refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:01:54 -04:00
c1408be9bb journal : aucune exception, la soumission suit son interrupteur
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:30:05 -04:00
c2fd6d17ca postfix : la soumission (465/587) n'est ouverte que la ou elle existe
Conditionnee a serveur_postfix_submission_actif, que seuls les locataires
levent. Le relais du site fait sortir le courriel des machines par le 25 ;
il n'a ni utilisateurs ni annuaire, et n'offre pas la soumission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:27:17 -04:00
e858fe60d3 journal : la frontiere reconciliee (8 regles et 2 alias retires)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:11:18 -04:00
ff518e18c2 journal : seulement_si, la sonde au site, la frontiere en attente
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:07:26 -04:00
25fd4fc6f1 flux : seulement_si non_vide ; forge 3000 et PowerDNS 5300 conditionnels
La forge du site sert son propre TLS (443) : le flux 3000 derriere un edge
ne vaut que si serveur_forgejo_tls est faux. L'AXFR 5300 ne vaut que la ou
l'instance publique existe (dns_public_site non vide) : pas au site.
Absente partout = vide ; une expression Jinja garde le statu quo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:01:46 -04:00
0318412a9e audit : rapport de preuves du 2026-10-01, carte a jour
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:01:41 -04:00
85932d948c connectivite : la liste hors de /etc/setops, le chemin du site, la simulation
Au site, l'inventaire est dynamique : le chemin de la liste pointait hors du
depot (meme surcharge que le ruleset). /etc/setops a deja ses gardiens : la
liste, qui n'a rien de secret, va dans /usr/local/lib/setops. L'activation
du minuteur ne s'execute plus en --check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:59:22 -04:00
34bfd43e06 flux : seulement_si — un flux peut dependre d'un reglage de l'ecosysteme
La vigie (8080) et la console (8090) n'ecoutent sur le reseau qu'en mode
locale ; en SSO, nginx est lie a 127.0.0.1 et la regle ne menait a rien.
Evaluee par ecosysteme dans les trois generateurs (nftables, Proxmox,
frontiere). Locataires : 8 regles Proxmox et 4 nftables en moins. Site :
inchange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:45:48 -04:00
209730b62a journal : premiere reconstruction sans arret ; parefeu par la sonde connectivite (135 s / 94 s)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:08:41 -04:00
8603ec4c19 connectivite : ignorer ses propres adresses ; un flux sans service est une information
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:50:35 -04:00
d2e2bb8990 connectivite : une sonde a la minute remplace la matrice du pare-feu
make flux ecrit, a cote de chaque .nft et depuis les memes regles, la
liste de ce que chaque VM doit joindre. La sonde (nftables_baseline) la
teste chaque minute avec la cause (rejete, delai, personne n'ecoute) ;
client_sante la porte en mode minute ; Icinga a une fraicheur par sonde.
eprouver_parefeu --sondes : tout activer d'un coup, juger par Icinga.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:46:49 -04:00
49a7e12850 journal : second arret de Technolibre, OOM sur l'AC, run_once
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:26:20 -04:00
b42bc51752 client_pki : l'empreinte de la racine se derive une fois, pas une fois par hote
Treize modules Python en parallele sur l'AC (765 Mo, 1 vCPU) : le noyau
a tue celui de collab-01 (rc=137) a la reconstruction de Technolibre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:25:08 -04:00
b5fa0e8d23 common_packages : apt-get attend le verrou dpkg lui aussi
lock_timeout ne protege que la verification du module ; apt-get n'attend
pas (APT 3.0 ne l'accorde qu'a la commande apt). Verrou repris par
unattended-upgrades sur mon-01 de Technolibre, ne une minute plus tot.
DPkg::Lock::Timeout pour tout appel d'APT, en premiere tache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:18:13 -04:00
2bf94acb96 reconstruire-locataire : les sous-commandes ecrivent sans tampon
L'etape parefeu est restee muette douze minutes : ecrivant dans un tuyau,
le Python enfant gardait sa sortie par blocs. PYTHONUNBUFFERED pour tous
les enfants, -u pour eprouver_parefeu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:25:14 -04:00
f425fb6cb3 journal : troisieme reconstruction de Chezlepro, arret au pare-feu, cause et correction
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:11:37 -04:00
20a5e44803 client_backup : le premier rapport a Icinga part d'un marqueur a lui ; le pare-feu ne compte que ses critiques
La copie de l'AC d'Icinga est deposee aussi par client_sante, plus tot :
sur une flotte neuve elle ne changeait jamais ici, et rien ne partait
(reconstruction de Chezlepro). client_backup retient desormais l'empreinte
de l'Icinga qui l'a entendu, apres le rapport. eprouver_parefeu ne s'arrete
plus sur les critiques presents avant l'activation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:08:55 -04:00
249d37d2c4 reconstruire-locataire : le nom de l'ecosysteme n'est plus demande deux fois
TENANT= le designe en toutes lettres ; le nom qu'exige raser en est
derive par raser.nom_court (une seule derivation). raser seul garde
son garde-fou.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:56:42 -04:00
a5a21ee4c5 reconstruire-locataire : le runner du site tire ses trois depots avant toute etape
Moteur, plan du locataire, depot du site (deduit d'underlay.yml), en
avance rapide seulement ; refus devant une modification locale. Vaut
aussi pour une reprise DEPUIS=creer ou inseminer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:35:23 -04:00
5a6d0f28f6 reconstruire-locataire : eprouve d'armer a bilan ; le suivi ne duplique plus le journal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:24:28 -04:00
14bf977bd6 reconstruction : make reconstruire-locataire conduit tout depuis le poste
Sauvegarder, raser/creer/inseminer (runner du site), armer (pause sauf
ARMER=oui), monter-flotte (runner du locataire), pare-feu, bilan.
Arret a la premiere etape en echec, reprise par DEPUIS=, journal dans
<locataire>/logs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:14:12 -04:00
cc906af918 reconstruction : monter-flotte et parefeu-*-flotte entrent dans le code
La sequence du runner (flux, socle, deployer-tout, valider) devient
make monter-flotte ; reconstruire s'appuie dessus. eprouver_parefeu.py
--flotte : toutes les VM une a une, runner en dernier, arret au premier
refus ; les reseaux des IPSet (zone d'administration) comptent enfin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 05:23:27 -04:00
9167f67c7b client_backup : le web frontal quitte le catalogue des detenteurs d'etat
Il relaie et ne sert rien lui-meme. Un noeud qui n'a plus rien a
sauvegarder perd aussi ses verifications de depot et de restauration,
qui rapportaient sinon a des services qu'Icinga ne declare plus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 04:46:19 -04:00
b135b09488 client_backup : un Icinga neuf recoit le premier rapport sans attendre dimanche
La copie de l'AC d'Icinga change quand Icinga ou le noeud est neuf : elle
declenche deposer, verifier le depot, verifier la restauration. Eprouve
sur web-frontal-01 de Technolibre ; second passage sans effet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 04:40:20 -04:00
844568af08 journal : Chezlepro reconstruite par les runners, l'etat est revenu a l'identique
AC (meme racine), sysadmin, annuaire, Nextcloud, DKIM, bases : empreintes
identiques avant et apres. Un defaut trouve et corrige (annuaire vierge).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 04:33:56 -04:00
d9a8905fc7 openldap : la mesure d'un annuaire vierge ne meurt plus sur lui
Sur un annuaire vraiment vierge, grep -v ne gardait aucune ligne et
sortait en 1 : pipefail faisait echouer la mesure dans le seul cas
qu'elle doit reconnaitre. Comptage par awk. Trouve par la
reconstruction de Chezlepro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 03:56:42 -04:00
515aac415a restauration : la section d'une base passe a psql par l'entree standard
postgres ne lit pas le repertoire jetable de root ; trouve en repetition
sur data-sql-01 de Technolibre. Le test l'exige desormais.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:51:19 -04:00
8a9da0c31a restauration : la reconstruction remet l'etat de l'incarnation precedente
Chaque role proprietaire (AC, bases, annuaire, Nextcloud, rspamd/DKIM,
courriel, forge, web) remet son etat au moment ou il le creerait neuf,
depuis le dernier instantane anterieur a la naissance de la machine.
La sauvegarde refuse de deposer tant qu'un etat d'avant attend.
Outil de noeud setops-restaurer ; cibles sauvegarder-maintenant,
restauration-etat, restauration-renoncer ; test_restauration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:39:22 -04:00
d3437a6308 journal : etat de Technolibre remis en place sans reconstruire
La reconstruction ne reinjecte aucun instantane : annuaire, Keycloak,
Nextcloud et courriel remis depuis restic-tech (23:50) sur les machines
existantes. Le runbook coupe aussi la section sur DROP DATABASE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:16:27 -04:00
502feff6e0 journal : reconstruction de Technolibre par les runners, quatre defauts trouves
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:46:35 -04:00
bdbf7d8317 keycloak : un client absent ne fait plus echouer les mappers de groupes et de roles
Sous set -euo pipefail, grep sans correspondance sortait AVANT la branche client absent.
Trouve par la reconstruction de Technolibre (plus de client forgejo).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:12:03 -04:00
f3e8883083 flux : sans voir son site, un ecosysteme qui en depend ne regenere pas ses pare-feux
Sur le runner d un locataire (ni underlay.yml ni plan du site), make flux reecrivait des
regles amputees de tout lien au site : AXFR du DNS public, collecte de la fabric,
insemination. Le generateur s abstient et le dit ; les regles committees font foi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:33:27 -04:00
be06def899 armement : le runner retrouve son identite SSH depuis la voute
Le runner fabriquait sa paire a sa naissance : reconstruit, il avait une identite que la
flotte ne connaissait pas (refuse sur les 13 machines de Technolibre). La cle privee vit
dans la voute du locataire ; serveur_ops_tenant la remet et exige que sa cle publique soit
declaree au plan (garde eprouvee en defaut).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:23:49 -04:00
cc3a4eec30 journal : 404 du frontal de Technolibre
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:12:30 -04:00
5f23201075 frontal : page 404 du locataire pour les noms qu il ne publie pas
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:37:00 -04:00
f818143b49 journal : page du PSPBT sur essai.chezlepro.ca
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:28:22 -04:00
b01de94493 waf : le mode dit en clair par la sonde ; journal du passage en blocage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:09:02 -04:00
4678ca0223 journal : plancher de Chezlepro avec essai.chezlepro.ca
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:03:16 -04:00
80bd8e88ac dorsal : confiance limitee a la forge du site ; chemin public eprouve de bout en bout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:34:44 -04:00
8186389309 domaine public -> frontal ; noms publics vers l adresse du locataire ; dorsal refuse les inconnus
Zone publique : les expositions pointent vers ip_publique (les NS restent au site).
client_pki : l AC interne ne signe que le domaine interne. Dorsal : serveur par defaut 444.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:14:56 -04:00
6d31842b83 journal : frontal proxy + WAF et dorsal des sites, poses chez les deux locataires
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:57:33 -04:00
cac0b75b3a web frontal : server_tokens deja actif chez Debian, pas redeclare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:53:08 -04:00
0cd08663e7 web frontal : le lien du vhost se simule au premier deploiement
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:50:23 -04:00
2fde5c7ae7 wiki publie : temoin a jour
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:48:43 -04:00
3fe7e3cea3 web frontal = reverse proxy + WAF ; le dorsal porte les sites
Le frontal derive ses vhosts des expositions publiques (meme derivation que l edge),
filtre par ModSecurity v3 + OWASP CRS (DetectionOnly d abord, eprouve), refuse les noms
inconnus, et ne sert plus rien lui-meme. Les sites statiques passent au dorsal, relaye
par l edge et par le frontal. Sondes : frontal (nouvelle), sites-servis suit le dorsal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:48:42 -04:00
2903d75831 journal : frontiere appliquee, NAT des locataires prouve dans les deux sens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:22:24 -04:00
5eca6ff4f8 journal : services publics des locataires prets ; frontiere en attente d accord
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:14:34 -04:00
37753984a7 services publics des locataires : web frontal 80/443, soumission 587/465 ; externe ouvre a tous chez les locataires
Le web frontal devient la porte publique (80/443 externe) ; Postfix publie la soumission
587 et ajoute le 465 (TLS direct). Le generateur nftables ouvrait un flux externe combine
a une source nommee a cette seule source : l Internet etait refuse par l hote. Corrige chez
les locataires ; au site, rien ne s elargit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:09:31 -04:00
e9b03399b9 frontiere : NAT des locataires par leur adresse publique (devis)
Sortie traduite avec l adresse attribuee par le site ; flux externe rediriges depuis cette
adresse vers la machine du role. Gardes : pas de redirection en double, adresse du pool.
Non applique : en attente de l accord de l exploitant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:54:36 -04:00
e84b155a5f pool d adresses publiques : le site attribue a chaque locataire la sienne
Declare dans opnsense.yml du site, rendu par le contrat (ip_publique), valide (une par
locataire, jamais celle du site), compare aux alias du WAN par frontiere-plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:24:09 -04:00
712a7da1fd frontiere : les flux admin traduits chez les tenants ; edge retire de l Internet
Le devis OPNsense ne rendait le chemin du poste chez un tenant qu au travers des flux
externe ; il traduit desormais les flux admin (gestion, VPN). Applique : 4 retraits
(WAN -> edges), 18 ajouts, 0 ecart. sonde_tcp : une fermeture propre apres la requete
est une reponse de la destination ; la mesure de la frontiere est conforme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:05:23 -04:00
81cdb84f66 journal : pare-feu Proxmox et nftables des edges poses ; route du poste vers le site
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:09:29 -04:00
2695013b32 proxmox-fw : les types ICMP au vocabulaire de Proxmox (fragmentation-needed)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:05:22 -04:00
e68f315625 wiki publie : temoin a jour
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:58:11 -04:00
1493059271 edge ferme a l Internet ; le pare-feu Proxmox recoit les flux externe
L edge n expose qu a sa zone d administration (plus de pair externe) ; le public passera
par le web frontal. Le devis Proxmox recoit les flux externe depuis +t<i>-internet (RFC 1918
exclus en nomatch) et depuis l admin quand le poste en est client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:58:01 -04:00
1bd211d62b resolution_interne : entre machines du site, chaque nom mene a son service
L edge expose les interfaces web aux personnes ; les VM du site vont directement au
service. Declare par domaine (service | edge, defaut edge) ; plancher, zone et certificat
du service suivent. Pose au site : 35 planchers sur 35 conformes ; locataires inchanges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:25:37 -04:00
efb1b3d334 wiki publie : temoin a jour
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:34:49 -04:00
eee6851c3b sonde plancher ; une exposition sans port n est plus attribuee a l edge
La zone publie l empreinte de ce que le plancher doit porter ; chaque machine la compare
a son /etc/hosts. Au site, elle a revele que sauvegarde, pki et dns.genese.internal
pointaient vers l edge (regression de mon deploiement du matin) : la derivation ne donne
plus a l edge ce qui n a pas de port. Zone du site corrigee, SFTP des locataires verifie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:34:47 -04:00
ba2020d486 powerdns : une zone plus recente que sa version servie est rechargee ; zones le voit
PowerDNS servait chez les deux locataires une zone anterieure a son fichier (console
absente) : le handler de rechargement avait ete abandonne. Reconciliation a chaque
passage (bind-reload-now) et controle dans la sonde zones. Plancher /etc/hosts rejoue
sur les deux locataires ; console se resout depuis les edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 05:29:45 -04:00
572 changed files with 27073 additions and 3250 deletions

3
.gitignore vendored
View file

@ -9,6 +9,9 @@ facts_cache/
**/vault.yml
# Underlay = fabric physique de l'operateur (cluster-global) ; gabarit public seul.
/underlay.yml
# Contexte actif de CETTE machine (`site:<depot>` ou `locataire:<depot>`) : propre a chaque
# poste et a chaque runner, jamais versionne. Voir scripts/contexte.py.
/contexte
*.secret
*.pem
*.key

994
AGENTS.md

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,24 +4,3 @@
`AGENTS.md` est la **source d'autorité unique** de ce dépôt. Claude Code doit le lire
et le respecter intégralement avant toute modification.
En cas de contradiction entre `CLAUDE.md` et `AGENTS.md`, **suivre `AGENTS.md`**.
Ce fichier est volontairement mince : il ne redéclare pas la doctrine (template, SSH,
pare-feu, cloud-init, handlers, Makefile, groupes…). Toute cette doctrine vit dans
`AGENTS.md`, pour éviter la duplication et les dérives qu'elle provoque.
## Les cinq règles absolues
1. **Un seul agent IA** travaille dans ce dépôt à la fois (Codex *ou* Claude, jamais les deux).
2. **`instance/inventories/*/hosts.yml` est un artefact GÉNÉRÉ** depuis le plan
(`instance/plan/`). On l'édite jamais à la main : on édite le plan, puis
`make instancier` → `make instancier-appliquer`.
3. **Aucun secret en clair** (mots de passe, clés privées, tokens, certificats) — Vault
ou stockage hors dépôt uniquement.
4. **Toute action destructive ou risquée exige une confirmation explicite** (ex.
`CONFIRMER=true`, `confirm_destructive_action: true`). Sans elle, refuser l'exécution.
5. **Rien n'est « prêt » sans validation.** Au minimum `--syntax-check` du playbook
touché, `ansible-lint` si disponible, et l'entrée `CHANGELOG.md` correspondante.
Le détail de chacune de ces règles, et tout le reste, est dans `AGENTS.md`.

292
Makefile
View file

@ -21,7 +21,16 @@ export SETOPS_INSTANCE ?= instance
#
# `?=` : une valeur deja posee dans l'environnement gagne — la GUI et les runners peuvent
# nommer leurs propres cles sans que ce fichier ait a les connaitre.
export ANSIBLE_VAULT_IDENTITY_LIST ?= $(shell python3 scripts/voutes.py identites 2>/dev/null)
#
# VIDE, ON N'EXPORTE RIEN (2026-10-04). Sur un clone sans aucune cle (la CI de la forge),
# `voutes.py` ne rend rien, et l'export posait quand meme `ANSIBLE_VAULT_IDENTITY_LIST=`.
# Ansible lit cette chaine vide comme un FICHIER de mot de passe, la resout en repertoire
# courant et refuse (« can not be a directory ») : `make ci` mourait a sa premiere commande
# sur un clone nu. Le poste a toujours des cles, il ne pouvait pas le voir.
SETOPS_VOUTES_IDENTITES := $(shell python3 scripts/voutes.py identites 2>/dev/null)
ifneq ($(strip $(SETOPS_VOUTES_IDENTITES)),)
export ANSIBLE_VAULT_IDENTITY_LIST ?= $(SETOPS_VOUTES_IDENTITES)
endif
# Inventaire de l'instance : un seul par instance dans le modèle « séparation par
# instance ». Détection rétro-compatible : principal > production > lab.
# Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml
@ -175,7 +184,7 @@ ansible-runtime: ## Prepare le repertoire temporaire local d'Ansible (prerequis
_instance-requise:
@if [[ ! -f "$(SETOPS_INSTANCE)/plan/serveurs.yml" ]]; then \
printf '%s\n' "Aucune instance configuree : '$(SETOPS_INSTANCE)/plan' introuvable."; \
printf '%s\n' "Demarre avec QUICKSTART.md. En bref :"; \
printf '%s\n' "Demarre avec README.md (demarrage rapide) puis docs/setup.md. En bref :"; \
printf '%s\n' " cp -r exemples/modeles/<modele> ../mon-instance && ln -s ../mon-instance instance"; \
printf '%s\n' " modeles disponibles : $$(ls exemples/modeles 2>/dev/null | grep -v '\.md' | tr '\n' ' ')"; \
exit 2; \
@ -185,7 +194,7 @@ _instance-requise:
aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste)
@printf '%s\n' 'Set-OPS — moteur d ecosystemes numeriques souverains'
@printf '%s\n' ''
@printf '%s\n' 'Nouveau ? -> QUICKSTART.md (de zero a ton ecosysteme sur Proxmox)'
@printf '%s\n' 'Nouveau ? -> README.md puis docs/setup.md (de zero a ton ecosysteme sur Proxmox)'
@printf '%s\n' 'Flux: editer le plan -> make instancier -> make instancier-appliquer -> make deployer'
@printf '%s\n' ''
@printf '%s\n' 'VM'
@ -193,7 +202,7 @@ aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste)
@printf '%s\n' ' make creer-vm HOTE=web-frontal-01'
@printf '%s\n' ' Cloner seulement, sans passer par le plan:'
@printf '%s\n' ' make cloner-vm HOTE=web-frontal-01 VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1'
@printf '%s\n' ' Configurer Proxmox et le Vault API (parametres: docs/config-proxmox.md):'
@printf '%s\n' ' Configurer Proxmox et le Vault API (parametres: docs/exploitation/config-proxmox.md):'
@printf '%s\n' ' make config'
@printf '%s\n' ''
@printf '%s\n' 'Hotes'
@ -230,10 +239,10 @@ aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste)
@printf '%s\n' ''
@printf '%s\n' 'Flux reseau (pare-feu / audit)'
@printf '%s\n' ' Matrice d audit + apercus nftables resolus (NON actives):'
@printf '%s\n' ' make flux # -> docs/registre-flux.md + instance/flux-genere/*.nft'
@printf '%s\n' ' make flux # -> docs/conception/registre-flux.md + instance/flux-genere/*.nft'
@printf '%s\n' ' make flux-verifier # valide schema + coherence de matrice'
@printf '%s\n' ''
@printf '%s\n' 'Remise au client — EN DEUX TEMPS (docs/remise-au-client.md)'
@printf '%s\n' 'Remise au client — EN DEUX TEMPS (docs/hebergement/remise-au-client.md)'
@printf '%s\n' ' Temps 1, l IDENTITE : le client gouverne ses gens des le premier jour.'
@printf '%s\n' ' make remise-recenser # ce qui partirait, sans rien ecrire'
@printf '%s\n' ' make ca-racine ; make ca-empreinte # sa racine d AC, et le temoin a comparer'
@ -308,15 +317,42 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
python3 scripts/tests/test_inventory_host.py
python3 scripts/tests/test_raser.py
python3 scripts/tests/test_raser_resultat.py
python3 scripts/tests/test_temoins.py
python3 scripts/tests/test_genome_colis.py
python3 scripts/tests/test_verifier_depot.py
python3 scripts/tests/test_apps_nextcloud.py
python3 scripts/tests/test_verrou_runner.py
python3 scripts/tests/test_adressage_derive.py
python3 scripts/tests/test_gui_intrants.py
python3 scripts/tests/test_runbooks.py
python3 scripts/tests/test_portee_console.py
python3 scripts/tests/test_forge_cles.py
python3 scripts/tests/test_devis_placement.py
python3 scripts/tests/test_sonder.py
python3 scripts/tests/test_fraicheur_icinga.py
python3 scripts/tests/test_sondes_conditionnelles.py
python3 scripts/tests/test_sondes_syntaxe.py
python3 scripts/tests/test_empreinte_plancher.py
python3 scripts/tests/test_expositions_sans_port.py
python3 scripts/tests/test_proxmox_fw_externe.py
python3 scripts/tests/test_libelles_parefeu.py
python3 scripts/tests/test_conditions_site.py
python3 scripts/tests/test_relais_du_site.py
python3 scripts/tests/test_sasl_local.py
python3 scripts/tests/test_dovecot_sonde_et_imap.py
python3 scripts/tests/test_parite_tls_site.py
python3 scripts/tests/test_exportateur_pg_tls.py
python3 scripts/tests/test_frontiere_pki.py
python3 scripts/tests/test_sonde_tcp.py
python3 scripts/tests/test_restauration.py
python3 scripts/tests/test_repertoires_partages.py
python3 scripts/tests/test_underlay_tunnel.py
python3 scripts/tests/test_face_publique_relais.py
python3 scripts/tests/test_mode_nat_sortant.py
python3 scripts/tests/test_verifier_flux_site.py
python3 scripts/tests/test_frontiere_refus.py
python3 scripts/tests/test_contexte.py
python3 scripts/tests/test_appels_locataire.py
# LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
@ -379,10 +415,10 @@ inseminer: ansible-runtime ## Le SITE amorce le runner d'un tenant — TENANT=<d
printf '\n%s\n' "$$hote porte desormais le moteur, son plan et la carte de la fabric."; \
printf '%s\n' "IL N EST PAS ARME : ni voute, ni cle — le site ne les detient pas."; \
printf '%s\n' "L armer est le geste d un humain, depuis son poste :"; \
printf '%s\n' " make appliquer GROUPE=serveur_ops_tenant (voir docs/filiation-emancipation.md)"
printf '%s\n' " make appliquer GROUPE=serveur_ops_tenant (voir docs/conception/filiation-emancipation.md)"
# PROUVER QU'UN LIEN DE FILIATION EST COUPE — la quatrieme ligne de
# `docs/filiation-emancipation.md`, celle qu'on oublie : « une emancipation non prouvee
# `docs/conception/filiation-emancipation.md`, celle qu'on oublie : « une emancipation non prouvee
# est une emancipation non faite ».
#
# GESTE DESTRUCTIF ET REVERSIBLE : il COUPE l'amont pendant quelques secondes pour voir
@ -536,8 +572,12 @@ hote-afficher: ansible-runtime ## Affiche tout ce que le plan derive pour un hot
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE)
appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
@if [[ -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
@# `-z "$(GROUPE)"` NE POUVAIT JAMAIS SE DECLENCHER (2026-10-03) : `GROUPE ?=
@# $(GROUPE_DEBIAN)` lui donne toujours une valeur. Une commande coupee par un retour
@# a la ligne (`make appliquer` seul) a ainsi applique `serveur_debian` a toute la flotte
@# de Technolibre. On refuse donc un groupe que l'operateur n'a pas DONNE lui-meme.
@if [[ "$(origin GROUPE)" == "file" || -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe (aucun groupe par defaut ici).'; \
exit 2; \
fi
@if [[ ! -f "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" ]]; then \
@ -545,7 +585,9 @@ appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
exit 2; \
fi
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-groupe --groupe $(GROUPE)
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)'
@# `ARGS` comme chez `site-appliquer` (2026-10-03) : simuler (`ARGS="--check --diff"`)
@# avant d'appliquer doit etre possible par la meme porte que l'application elle-meme.
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)' $(ARGS)
# ATTENDRE PLUTOT QUE CONSTATER (2026-08-27). `deployer` faisait un `ping` unique
# (`_verifier-acces-hote`). Depuis que `creer-vm` ne guette plus le SSH — il prouve la
@ -587,7 +629,7 @@ site: ansible-runtime ## Regenere playbooks/site.yml depuis les couches et le gr
site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declarees
python3 scripts/orchestrer.py verifier
.PHONY: flux flux-verifier
.PHONY: flux flux-verifier fiches-site-deposer
cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire)
python3 scripts/exporter_cles.py --recenser
@ -723,8 +765,8 @@ proxmox-fw-activer-vm: ansible-runtime ## Active le pare-feu d'UNE VM en le prou
.PHONY: proxmox-fw-eprouver proxmox-fw-activer-vm
.PHONY: placement-plan sdn-plan sdn-appliquer
placement-plan: ansible-runtime ## Le noeud, stockage, pont et gabarit du tenant existent-ils sur ce cluster ? (aucune ecriture)
python3 scripts/devis_placement.py
placement-plan: ansible-runtime ## Le noeud, stockage, pont et gabarit du tenant existent-ils sur ce cluster ? (aucune ecriture) [TENANT=<depot>]
python3 scripts/devis_placement.py $(if $(TENANT),--locataire $(TENANT))
sdn-plan: ansible-runtime ## Ecart entre le SDN EVPN (+ sortie des VRF) et son devis (aucune ecriture)
python3 scripts/appliquer_sdn.py
@ -847,6 +889,17 @@ site-intrants: ## Les intrants que le SITE monte expose a ses locataires (derive
site-intrants-verifier: ## Le locataire monte suit-il encore son site ? (aucune ecriture)
python3 scripts/site_intrants.py --verifier
face-reseau-publier: ## Chaque locataire publie sa face reseau pour son site (a commiter chez lui, apres `make flux`)
@# LE LOCATAIRE PUBLIE, LE SITE LIT (2026-10-05). Le site lisait les fichiers internes de ses
@# locataires ; il lit desormais `face-reseau.yml`. P92 refuse une face perimee.
python3 scripts/contexte.py --publier-faces
fiches-site-deposer: ## Le site depose chez chacun de ses locataires la fiche qu'il lui destine (a commiter chez eux)
@# LE SITE DEPOSE, LE LOCATAIRE LIT (2026-10-05). Le runner d'un locataire n'a pas le depot
@# de son site : ce que le site lui attribue et lui offre lui parvient par ce fichier,
@# `fiche-site.yml`, que l'instancier lit. P91 refuse une fiche perimee.
python3 scripts/contexte.py --deposer-fiches
.PHONY: vpn-admin-plan vpn-admin-appliquer
vpn-admin-plan: ## Acces WireGuard des admins : ce que la frontiere porte face au plan (aucune ecriture)
python3 scripts/vpn_admin.py plan
@ -896,6 +949,39 @@ flux-verifier: ## Verifie que le registre des flux correspond aux meta/flux.yml
valider: ansible-runtime ## Passe la recette de validation sur la flotte
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml
.PHONY: restauration-etat restauration-renoncer sauvegarder-maintenant temoins-etat
sauvegarder-maintenant: ansible-runtime ## Depose l'etat de chaque noeud TOUT DE SUITE (avant de raser) — HOTE=<hote> optionnel
@set -e; $(VAULT_UNE_FOIS) \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
-e restauration_action=sauvegarder $(if $(HOTE),--limit "$(HOTE)")
# LA RESTAURATION DANS LA RECONSTRUCTION (2026-09-30) — la remise elle-meme n'a pas de
# cible : chaque role proprietaire la fait au deploiement. Ces deux cibles-ci servent a
# VOIR ce qui attend, et a ECARTER un etat qu'on ne veut pas remettre.
restauration-etat: ansible-runtime ## Ce qui attend dans le depot, et ce que chaque jeu est devenu — HOTE=<hote> optionnel
@set -e; $(VAULT_UNE_FOIS) \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
$(if $(HOTE),--limit "$(HOTE)")
# LES TEMOINS (2026-10-07) — l'etat remis est-il celui d'avant ? Compare l'etat vivant a
# l'instantane etiquete au rasage (`sauvegarder-maintenant`). `SOURCE=candidat` compare au
# dernier instantane d'avant la naissance du noeud, faute d'etiquette. Lecture seule.
temoins-etat: ansible-runtime ## L'etat vivant est-il celui d'avant le rasage ? (lecture seule) — HOTE= SOURCE=candidat optionnels
@set -e; $(VAULT_UNE_FOIS) \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
-e restauration_action=temoins $(if $(SOURCE),-e restauration_source="$(SOURCE)") \
$(if $(HOTE),--limit "$(HOTE)")
restauration-renoncer: ansible-runtime ## Ecarte l'etat anterieur d'un jeu, sans le remettre — HOTE= JEU= CONFIRMER=true
@set -e; \
if [[ "$(CONFIRMER)" != "true" || -z "$(HOTE)" || -z "$(JEU)" ]]; then \
printf '%s\n' 'Refus: ECARTER un etat anterieur (il sortira de la retention). HOTE=, JEU= et CONFIRMER=true requis.'; \
exit 2; \
fi; \
$(VAULT_UNE_FOIS) \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
--limit "$(HOTE)" -e restauration_action=renoncer -e restauration_jeu="$(JEU)"
.PHONY: schema
schema: ## Regenere docs/audit/schema-plan.json depuis les registres et les validateurs
python3 scripts/schema_plan.py
@ -1080,14 +1166,23 @@ define VAULT_UNE_FOIS
endef
flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n
# LE SITE MATERIALISE UN LOCATAIRE NOMME, D'APRES SA FACE (2026-10-05, docs/conception/conception-contextes.md,
# materialisation M2). Avec `TENANT=<depot>`, la liste des machines et leurs parametres de clonage
# viennent de `face-reseau.yml` que le locataire publie — rendus a l'octet pres comme
# `parametres-proxmox` (P93) — et aucune instance n'est montee. Sans `TENANT`, l'ancienne lecture
# de l'instance montee (le poste, le laboratoire).
LISTE_ACTIFS = $(if $(TENANT),python3 scripts/contexte.py --hotes-actifs $(TENANT),python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) lister-actifs)
PARAMS_CLONAGE = $(if $(TENANT),python3 scripts/contexte.py --parametres-clonage $(TENANT) --hote,python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote)
INSTANCE_OU_LOCATAIRE = $(if $(TENANT),,_instance-requise)
flotte-creer: $(INSTANCE_OU_LOCATAIRE) ## Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: creation de TOUTES les VM actives du plan (clone Proxmox).'; \
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
exit 2; \
fi; \
hotes="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) lister-actifs)"; \
hotes="$$($(LISTE_ACTIFS))"; \
if [[ -z "$$hotes" ]]; then printf '%s\n' 'Refus: aucun hote actif dans le plan.'; exit 2; fi; \
par="$(PARALLELE)"; par="$${par:-4}"; \
tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \
@ -1096,7 +1191,7 @@ flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le
n=0; \
for h in $$hotes; do \
printf '\n=== Creation VM: %s ===\n' "$$h"; \
( $(MAKE) creer-vm HOTE="$$h" > "$$tmp/$$h.log" 2>&1; printf '%s' "$$?" > "$$tmp/$$h.rc" ) & \
( $(MAKE) creer-vm HOTE="$$h" $(if $(TENANT),TENANT="$(TENANT)") > "$$tmp/$$h.log" 2>&1; printf '%s' "$$?" > "$$tmp/$$h.rc" ) & \
n=$$((n+1)); \
if (( n >= par )); then wait -n || true; n=$$((n-1)); fi; \
done; \
@ -1111,6 +1206,13 @@ flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le
if (( echec )); then printf '\nAu moins une VM n a pas ete creee — rien ne continue.\n'; exit 2; fi; \
printf '\nToutes les VM actives sont creees.\n'
locataire-creer: ## Le SITE cree les VM d'un locataire d'apres sa face reseau — TENANT=<depot> CONFIRMER=true [PARALLELE=n]
@if [[ -z "$(TENANT)" ]]; then printf '%s\n' 'Refus: TENANT=<depot du locataire> requis.'; exit 2; fi
@$(MAKE) --no-print-directory flotte-creer TENANT="$(TENANT)" CONFIRMER="$(CONFIRMER)" PARALLELE="$(PARALLELE)"
locataire-raser: ## DESTRUCTIF : le SITE rase les VM d'un locataire d'apres sa face reseau — TENANT=<depot> CONFIRMER=true INSTANCE=<nom court> [HOTE=h]
@if [[ -z "$(TENANT)" ]]; then printf '%s\n' 'Refus: TENANT=<depot du locataire> requis.'; exit 2; fi
@$(MAKE) --no-print-directory raser TENANT="$(TENANT)" CONFIRMER="$(CONFIRMER)" INSTANCE="$(INSTANCE)" HOTE="$(HOTE)"
# --- LES MACHINES DU SITE -----------------------------------------------------
#
# UN SITE N'EST PAS UN PLAN. Un tenant se derive de son `index` ; un site n'en a pas et
@ -1132,6 +1234,10 @@ site-inventaire: ## L'inventaire dynamique du site, tel qu'Ansible le voit
.PHONY: site-appliquer
site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops_site|...>
@if [[ "$(origin GROUPE)" == "file" || -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe (aucun groupe par defaut ici).'; \
exit 2; \
fi
@# `GROUPE` a un defaut global (GROUPE_DEBIAN) : tester s'il est VIDE ne protege de
@# rien, ce garde-fou-la ne pourrait jamais se declencher. Le vrai risque, observe :
@# lancer un playbook de TENANT contre l'inventaire du SITE. Ansible n'y trouve aucun
@ -1144,6 +1250,8 @@ site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops
@# `underlay.vault.yml`, qui vit a cote de sa carte et hors depot. Sans le `-e @`, le
@# role echoue sur son assertion — et l'echec ne dit pas qu'il manque un FICHIER,
@# seulement que les valeurs sont vides.
@# `SETOPS_UNDERLAY` (2026-10-13) : la carte lue par l'inventaire DESIGNE aussi la voute.
@# Lire le symlink seul aurait charge les secrets de Chezlepro contre TechnoLibre.
@set -e; \
if [[ ! -f "playbooks/groupes/$(GROUPE).yml" ]]; then \
printf 'Refus: aucun playbook pour le groupe %s.\n' "$(GROUPE)"; exit 2; \
@ -1155,7 +1263,7 @@ site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops
printf 'Groupes du site : %s\n' "$$connus"; \
exit 2; \
fi; \
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
ansible-playbook -i scripts/site_inventaire.py "playbooks/groupes/$(GROUPE).yml" \
"$${supp[@]}" $(ARGS)
@ -1177,6 +1285,12 @@ fiches: ## Regenere la fiche de chaque role (docs/roles/) — ROLE=<nom> pour un
.PHONY: site-deployer-tout
site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des couches — CONFIRMER=true
@# L'AMORCAGE DU CACHE (2026-10-13). Toute machine du site s'amorce sur le cache des son
@# socle — or le cache n'existe qu'apres le sien. Aucune COUCHE ne peut le dire : une couche
@# vaut pour toutes les machines a la fois. Le premier site construit depuis zero
@# (TechnoLibre) l'a montre ; la machine du cache passe donc d'abord, seule, par son socle
@# et son cache. Sur un site deja monte, ce passage ne change rien. Les regles nftables
@# generees sont exigees avant tout (`verifier_flux_site.py`) : sans elles, repli generique.
@# LE SITE AVAIT L'ORCHESTRATION, PAS LE MOYEN DE LA LANCER (2026-09-14).
@#
@# `playbooks/site.yml` est genere par `orchestrer.py` et ordonne les couches pour
@ -1206,11 +1320,21 @@ site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des cou
exit 2; \
fi; \
python3 scripts/verifier_genome_a_jour.py || exit 2; \
python3 scripts/verifier_flux_site.py || exit 2; \
python3 scripts/orchestrer.py verifier; \
python3 scripts/orchestrer.py ecrire; \
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \
cache="$$(python3 scripts/site_inventaire.py --list | python3 -c \
'import json,sys; print(" ".join((json.load(sys.stdin).get("serveur_cache_site") or {}).get("hosts") or []))')"; \
if [[ -n "$$cache" ]]; then \
printf '\n=== Amorcage : le cache du site d abord (%s) ===\n' "$$cache"; \
for g in serveur_debian serveur_durci serveur_artefacts serveur_cache_site; do \
ansible-playbook -i scripts/site_inventaire.py "playbooks/groupes/$$g.yml" \
--limit serveur_cache_site "$${supp[@]}" $$mode $(ARGS); \
done; \
fi; \
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml \
--limit "$(GROUPE_HOTES_ACTIFS)" "$${supp[@]}" $$mode $(ARGS)
@ -1264,7 +1388,7 @@ gabarit-etat: ## Le gabarit porte-t-il ce que le SITE declare ? — ne corrige r
.PHONY: genome-etat
genome-etat: ## Ce que la forge du site porte, face au poste — ne corrige rien, regarde
@set -e; \
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/genome_etat.yml "$${supp[@]}" $(ARGS)
@ -1279,11 +1403,31 @@ publier: ## Publie le genome d'un geste : eregion PUIS la forge du site, et le v
.PHONY: publier
genome-pousser: ## Le runner du site pousse le genome sur la forge du site — DEPOT=<nom> pour un seul
@set -e; \
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
SETOPS_DEPOT="$(DEPOT)" ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/genome_pousser.yml "$${supp[@]}" $(ARGS)
.PHONY: forge-cles
forge-cles: ## Le site enregistre sur le depot de chaque locataire la cle de lecture que sa face publie — CONFIRMER=true pour ecrire
@set -e; \
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
CONFIRMER="$(CONFIRMER)" ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/forge_cles.yml "$${supp[@]}" $(ARGS)
.PHONY: forge-comptes
forge-comptes: ## Les comptes de la forge du site : un par mainteneur (plan du site), et le lecteur de la console — CONFIRMER=true pour ecrire
@set -e; \
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
CONFIRMER="$(CONFIRMER)" ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/forge_comptes.yml "$${supp[@]}" $(ARGS)
.PHONY: forge-etancheite
forge-etancheite: ansible-runtime ## La forge du site laisse-t-elle chaque runner lire ses depots, et personne d'autre ? (mesure, aucune ecriture)
python3 scripts/forge_etancheite.py
.PHONY: depots-perimes
depots-perimes: ansible-runtime _instance-requise ## Depots qu'un runner ne declare plus : regarde, et retire avec CONFIRMER=true
@# DESTRUCTIF SUR CONFIRMATION SEULEMENT. Sans `CONFIRMER=true`, ce geste REGARDE :
@ -1355,6 +1499,9 @@ site-creer: ## Cree les machines du site depuis l'underlay — CONFIRMER=true
NOEUD_PROXMOX="$$SETOPS_NOEUD" \
VMID_MODELE="$${SETOPS_VMID_MODELE:-$(VMID_MODELE)}" \
CLONE_COMPLET="$$SETOPS_CLONE_COMPLET" \
CLE_SSH_PUBLIQUE="$${SETOPS_CLE_PUBLIQUE:-$(CLE_SSH_PUBLIQUE)}" \
DNS="$${SETOPS_DNS:-$(DNS)}" \
DOMAINE_RECHERCHE="$${SETOPS_DOMAINE:-$(DOMAINE_RECHERCHE)}" \
FORMAT_DISQUE="$(FORMAT_DISQUE)"; \
printf '=== %s : %s s ===\n' "$$m" "$$(( $$(date +%s) - t0 ))"; \
done; \
@ -1372,8 +1519,8 @@ _attendre-flotte: ansible-runtime
printf 'Flotte joignable.\n'
.PHONY: reconstruire
raser: ansible-runtime _instance-requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom>
@python3 scripts/raser.py $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer)
raser: ansible-runtime $(INSTANCE_OU_LOCATAIRE) ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom> [TENANT=<depot>]
@python3 scripts/raser.py $(if $(TENANT),--locataire $(TENANT)) $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer)
# `make flux` D'ABORD, et ce n'est pas une precaution : sans lui, `instance/flux-genere/`
# est VIDE et le socle deploie nftables en `policy drop` SANS AUCUNE REGLE DERIVEE. La
@ -1394,8 +1541,70 @@ reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM pu
$(MAKE) flux; \
$(MAKE) flotte-creer CONFIRMER=true; \
$(MAKE) _attendre-flotte; \
$(MAKE) _amorcer-socle; \
$(MAKE) deployer-tout CONFIRMER=true
$(MAKE) monter-flotte CONFIRMER=true
# MONTER UNE FLOTTE DONT LES VM EXISTENT DEJA (2026-09-30). C'est la seconde moitie de
# `reconstruire`, et TOUT ce que le runner d'un locataire peut en faire : il ne cree pas de VM
# (l'API du cluster est a l'hebergeur), il les recoit, armees, et les monte. Jusqu'ici cette
# sequence vivait dans un script ecrit a la main dans /tmp du runner — deux reconstructions
# l'ont portee, et aucune trace ne la gardait.
#
# `set -e` : la premiere etape en echec arrete tout. Chaque etape est horodatee, pour qu'un
# journal dise ou l'on en etait sans relire dix mille lignes d'Ansible. `valider` fait partie
# de la sequence : une flotte sans recette n'est pas montee, elle est deployee.
.PHONY: monter-flotte
monter-flotte: _instance-requise ## Monte une flotte deja creee : flux, AC et DNS, tout le reste, recette — CONFIRMER=true
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: MONTER la flotte (deploiement complet puis recette). Relancer avec CONFIRMER=true.'; \
exit 2; \
fi; \
etape() { printf '\n=== ETAPE %s %s\n' "$$1" "$$(date +%T)"; }; \
etape flux; $(MAKE) flux; \
etape _amorcer-socle; $(MAKE) _amorcer-socle; \
etape deployer-tout; $(MAKE) deployer-tout CONFIRMER=true; \
etape valider; $(MAKE) valider; \
printf '\n=== FLOTTE MONTEE %s\n' "$$(date +%T)"
# RECONSTRUIRE UN LOCATAIRE DE BOUT EN BOUT, DEPUIS LE POSTE (2026-09-30). Les deux
# reconstructions du 2026-09-30 passaient par cinq endroits et des commandes tapees a la
# main ; la sequence vit desormais dans `scripts/reconstruire_locataire.py` (voir son
# en-tete). `TENANT=` designe l'ecosysteme en toutes lettres : le nom qu'exige `raser` en est
# derive, pas redemande. L'armement marque une pause, sauf `ARMER=oui`. `DEPUIS=<etape>` reprend apres un
# arret. Le journal complet va dans `<locataire>/logs/`.
.PHONY: reconstruire-locataire
reconstruire-locataire: ## Rase et reconstruit un locataire, du site a la recette, etat remis — TENANT=OPS-X CONFIRMER=true
@if [[ -z "$(TENANT)" ]]; then \
printf '%s\n' 'Refus: TENANT=<OPS-X> requis (le dossier du locataire, en toutes lettres).'; \
exit 2; \
fi
python3 scripts/reconstruire_locataire.py --tenant "$(TENANT)" \
$(if $(filter true,$(CONFIRMER)),--confirmer) $(if $(DEPUIS),--depuis "$(DEPUIS)") \
$(if $(filter oui,$(ARMER)),--armer)
# LE PARE-FEU PROXMOX D'UNE FLOTTE RECONSTRUITE (2026-09-30). Une VM clonee nait sans les
# options de pare-feu : chaque reconstruction les perd. La procedure (une VM a la fois,
# matrice avant/apres, Icinga) etait rejouee par une boucle tapee a la main ; `--flotte` la
# porte, avec la regle qui la rend sure : arret au premier refus.
#
# PAR LES SONDES DEPUIS LE 2026-09-30 (`--sondes`) : la matrice prenait une vingtaine de
# minutes en fin de reconstruction. Chaque VM teste desormais chaque minute ses flux promis
# (sonde `connectivite`) ; on active tout d'un coup et on lit Icinga. La matrice reste pour
# le diagnostic : `python3 scripts/eprouver_parefeu.py --instance OPS-X --hote <vm> --plan`.
#
# DEPUIS LE POSTE : la matrice se joue avec les acces du LOCATAIRE, l'activation passe par
# le runner du SITE, seul a joindre l'API du cluster. Le poste est le seul a tenir les deux.
.PHONY: parefeu-verifier-flotte parefeu-activer-flotte
parefeu-verifier-flotte: ## La sonde `connectivite` est-elle saine sur chaque VM (prealable a l'activation) — INSTANCE=OPS-X
@if [[ -z "$(INSTANCE)" ]]; then printf '%s\n' 'Refus: INSTANCE=<OPS-X> requis.'; exit 2; fi
python3 scripts/eprouver_parefeu.py --instance "$(INSTANCE)" --flotte --sondes --plan
parefeu-activer-flotte: ## Active le pare-feu Proxmox de toutes les VM, juge par la sonde `connectivite` — INSTANCE=OPS-X CONFIRMER=true
@if [[ -z "$(INSTANCE)" || "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: INSTANCE=<OPS-X> et CONFIRMER=true requis (active le pare-feu de TOUTES les VM, une a une).'; \
exit 2; \
fi
python3 scripts/eprouver_parefeu.py --instance "$(INSTANCE)" --flotte --sondes --activer
# « Go ahead, make my day. » — LE bouton rouge : alias de reconstruire (Clint Eastwood).
# Cree toutes les VM puis deploie tout, en une commande. Garde CONFIRMER=true.
@ -1420,8 +1629,12 @@ _amorcer-socle: ansible-runtime _instance-requise
myDay: reconstruire ## Alias strict de `reconstruire` (meme cible, memes gardes)
deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE=<groupe>
@if [[ -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
@# `-z "$(GROUPE)"` NE POUVAIT JAMAIS SE DECLENCHER (2026-10-03) : `GROUPE ?=
@# $(GROUPE_DEBIAN)` lui donne toujours une valeur. Une commande coupee par un retour
@# a la ligne (`make appliquer` seul) a ainsi applique `serveur_debian` a toute la flotte
@# de Technolibre. On refuse donc un groupe que l'operateur n'a pas DONNE lui-meme.
@if [[ "$(origin GROUPE)" == "file" || -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe (aucun groupe par defaut ici).'; \
exit 2; \
fi
$(MAKE) appliquer GROUPE="$(GROUPE)"
@ -1456,18 +1669,6 @@ verifier-deploiement: ansible-runtime ## Verifie l'etat de la flotte apres deplo
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)" --check --diff; \
done
# LE CLONAGE SE SERIALISE PAR STOCKAGE, et c'est la granularite de Proxmox
# lui-meme : il tient un verrou PAR STOCKAGE pendant l'allocation du disque. Deux
# clones simultanes sur le meme stockage se le disputent, et celui qui attend trop
# rend « clone failed: can't lock file '/var/lock/pve-manager/pve-storage-<nom>' -
# got timeout » — un message qui nomme le verrou, jamais le parallelisme qui l'a
# provoque. A deux VM rien ne parait ; a cinq, une creation sur cinq echoue, et la
# reconstruction s'arrete sans que rien ne dise pourquoi elle tenait a deux.
#
# LA CLE PORTE LE NOEUD ET LE STOCKAGE, donc deux stockages distincts clonent
# TOUJOURS en parallele : `flotte-creer PARALLELE=n` garde tout son sens, et seule
# la section qui contend est prise un a la fois, dans l'ordre ou la sequence du
# plan la demande.
cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom> VMID=<id>
@if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \
@ -1501,7 +1702,7 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom>
-e proxmox_clone_nom="$(HOTE)" \
-e proxmox_clone_vmid="$(VMID)" \
-e proxmox_clone_ipconfig0="$$ipconfig" \
-e proxmox_clone_pool="$(if $(POOL),$(POOL),$$(python3 scripts/devis_proxmox_pools.py --pool-actif))" \
-e proxmox_clone_pool="$(if $(POOL),$(POOL),$$(python3 scripts/devis_proxmox_pools.py $(if $(TENANT),--pool-du-locataire $(TENANT),--pool-actif)))" \
); \
[[ -n "$(VMID_MODELE)" ]] && extra_vars+=( -e proxmox_clone_vmid_modele="$(VMID_MODELE)" ); \
[[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \
@ -1530,7 +1731,7 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom>
done; \
done; \
if [[ -z "$$vault_file" && -L underlay.yml ]]; then \
vault_file="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
vault_file="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
fi; \
if [[ -n "$$vault_file" && -f "$$vault_file" ]]; then \
read -r premiere_ligne < "$$vault_file" || true; \
@ -1542,16 +1743,15 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom>
;; \
esac; \
fi; \
verrou="$${TMPDIR:-/tmp}/setops-clone-$$(printf '%s_%s' '$(NOEUD_PROXMOX)' '$(STOCKAGE_PROXMOX)' | tr -c 'A-Za-z0-9._-' '_').lock"; \
flock "$$verrou" ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}"
ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}"
creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable — HOTE=<nom>
creer-vm: $(INSTANCE_OU_LOCATAIRE) ## Cree une VM et attend qu'elle soit joignable — HOTE=<nom> [TENANT=<depot>]
@set -e; \
if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \
exit 2; \
fi; \
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
params="$$($(PARAMS_CLONAGE) $(HOTE))"; \
eval "$$params"; \
export SETOPS_CLES_AMORCAGE; \
$(MAKE) cloner-vm \
@ -1592,7 +1792,7 @@ creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable —
@# ATTENDRE LA DISPONIBILITE est un autre geste, deplace chez `deployer` : cloud-init
@# et dpkg se guettent juste avant de configurer, par celui qui va configurer.
@if [[ "$(ATTENDRE)" != "false" ]]; then \
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
params="$$($(PARAMS_CLONAGE) $(HOTE))"; \
eval "$$params"; \
python3 scripts/attendre_materialisation.py \
--vmid "$$SETOPS_VMID" --ip "$$SETOPS_IP" --hote "$(HOTE)" \

View file

@ -1,133 +0,0 @@
# Démarrage rapide — de zéro à ton écosystème souverain
Tu débarques avec une **grappe Proxmox vierge** et tu veux monter ton écosystème
numérique. Voici le chemin, de bout en bout. Set-OPS est le **moteur** ; tu vas
créer **ton instance** à partir d'un modèle, puis déployer.
> Concepts en 10 s : tu **décris un plan** (quelles VM, quels services), le moteur
> **génère l'inventaire**, et Ansible **déploie**. Tu n'édites jamais l'inventaire
> à la main. Détails : `docs/plan-et-generation.md`.
## 0. Prérequis (à toi de fournir)
- une **grappe Proxmox** où tu es admin (API activée) ;
- une machine de pilotage Linux avec **Ansible**, **Python 3**, **make**, `git`
(et `node` pour le garde-fou JS du GUI, optionnel) ;
- un **token API Proxmox** ;
- une **paire de clés SSH** (la publique ira dans les VM via cloud-init) ;
- un **mot de passe Ansible Vault** (pour chiffrer tes secrets).
## 1. Cloner le moteur
```bash
git clone <url-de-Set-OPS> Set-OPS && cd Set-OPS
```
## 2. Choisir un modèle et créer ton instance
Le dépôt public fournit **un modèle générique : `socle`** — quatre VM, le minimum
souverain : PKI (`step-ca`), DNS interne (PowerDNS), edge TLS (nginx) et **magasin
courriel** (Dovecot). C'est un magasin de boîtes, pas un relais : le MTA (`serveur_postfix`)
s'ajoute ensuite, comme les autres modules. Les
modèles **assemblés** par offre (`identite`, `observabilite`, `forge`, `collaboration`,
`presence-web`, `integral`) sont un actif à part, dans le dépôt privé `Set-OPS-modeles`
(cf. [`exemples/modeles/README.md`](exemples/modeles/README.md)).
```bash
cp -r exemples/modeles/socle ../mon-instance # ton instance, ailleurs
ln -s ../mon-instance instance # le moteur la trouve via ce lien
```
*(Alternative au symlink : `export SETOPS_INSTANCE=../mon-instance`.)*
## 3. Renseigner ton instance (« tes couleurs »)
> Les chemins ci-dessous disent `production/` parce que **c'est le nom que porte le modèle
> que tu viens de copier**. Ce n'est pas un nom imposé : le moteur cherche `principal`
> d'abord, `production` ensuite. Si tu lis ailleurs `<inventaire>`, c'est cette place-là.
- `instance/inventories/production/group_vars/all/10-intrants.yml` → **`domaine_interne`** (ex. `monorg.internal`) ;
- `instance/plan/nomenclature.yml` → ton **supernet** (ex. `10.20.0.0/16`) ;
- `instance/plan/domaines.yml` → ton **domaine public** ;
- `instance/plan/serveurs.yml` → **placement Proxmox** (nœud, stockage, disque, mémoire, cœurs).
Tu peux aussi le faire dans le GUI plus tard (`make inventaire-ui`).
## 4. Configurer Proxmox et tes secrets
```bash
make config # renseigne API host/user/port, nœud, stockage, VMID du template...
```
Chaque paramètre demandé est expliqué dans [`docs/config-proxmox.md`](docs/config-proxmox.md).
Tous tes secrets (token API + `vault_*`) vont dans **une voûte unique par instance** :
`instance/inventories/production/group_vars/all/vault.yml`, à partir du
gabarit [`exemples/vault.exemple.yml`](exemples/vault.exemple.yml), chiffrée avec
`ansible-vault`.
**Une voûte, une clé** (depuis le 2026-08-28). Le mot de passe de ta voûte se dépose dans un
fichier dont le nom est **dérivé du dossier de ton instance**, en minuscules :
```bash
# instance dans ../mon-instance -> clé dans ~/.config/setops-vault-mon-instance
install -m 600 /dev/null ~/.config/setops-vault-mon-instance
$EDITOR ~/.config/setops-vault-mon-instance # ta phrase de passe, une seule ligne
python3 scripts/voutes.py etat # vérifie que le moteur la trouve
```
Il n'y a **rien à exporter** : le `Makefile` construit `ANSIBLE_VAULT_IDENTITY_LIST` en
appelant `scripts/voutes.py`. Un seul `ANSIBLE_VAULT_PASSWORD_FILE` ne suffirait plus de
toute façon — créer une VM ouvre **deux** voûtes dans la même exécution : la tienne, et
celle de l'hébergeur qui détient le jeton Proxmox.
## 5. Construire le golden template Debian 13 (UNE seule fois)
Une grappe vierge n'a aucun template. Crée une VM **Debian 13 vanille**, rends-la
joignable par Ansible, puis :
```bash
make preparer-modele # socle + durcissement + cloud-init + qemu-guest-agent...
make verifier-modele
make nettoyer-modele CONFIRMER=true
```
Convertis ensuite la VM en **template Proxmox**. Son nom doit être celui que `make config`
a enregistré comme *nom logique du modèle* — **`modeleSetOPS`** par défaut
(`proxmox_clone_source_nom`). Un nom qui ne correspond pas se solde par un clonage qui ne
trouve pas sa source. Détails et procédure : `docs/vm-lifecycle.md` et
`docs/procedure-template-debian13-proxmox.md`.
## 6. Générer ton inventaire depuis le plan
```bash
make instancier # montre ce que le plan produit (diff)
make instancier-appliquer FORCE=1 # 1re génération : écrit instance/inventories/production/hosts.yml
```
Tes hôtes sont là, en état `planifie`. `make serveurs` te montre leurs VMID/IP dérivés.
## 7. Créer les VM (clone du template + cloud-init)
Pour chaque hôte — `creer-vm` lit ses VMID/IP/VLAN/passerelle directement dans
l'inventaire généré (`make serveurs` te les montre) :
```bash
make creer-vm HOTE=infra-dns-01
```
*(Les valeurs de nœud/stockage/template viennent de `make config`. La création
suit désormais le plan de bout en bout : un seul argument, `HOTE`.)*
## 8. Activer puis déployer
Passe l'hôte en `etat: actif` dans `instance/plan/serveurs.yml`, régénère, déploie :
```bash
make instancier-appliquer
make deployer HOTE=infra-dns-01 # configure l'hôte selon ses groupes
# ou, par couche :
make deployer-groupe GROUPE=serveur_powerdns
```
Les déploiements de groupe ne ciblent **que** les hôtes actifs.
## 9. Ensuite : tu vis dans le plan
Édite le plan (GUI `make inventaire-ui`, ou les registres `instance/plan/`),
`make instancier-appliquer`, `make deployer`. Tu ne touches jamais `hosts.yml`.
## Valider à tout moment
```bash
make inventaire-verifier # registres + inventaire + garde-fous
make verifier # + ansible-lint + --syntax-check
```
> Ces cibles chargent l'inventaire complet : pose d'abord la clé de ta voûte
> (étape 4), sinon Ansible s'arrête sur
> « Attempting to decrypt but no vault secrets found ».
---
Ordre de mise en place des services : `docs/catalogue-services.md`. Modèle,
registres et règle d'or : `docs/plan-et-generation.md` et `AGENTS.md`.

257
README.md
View file

@ -1,231 +1,60 @@
# Set-OPS — moteur d'écosystèmes numériques souverains
**Set-OPS est un moteur Ansible générique** qui permet à un hébergeur de **construire et exploiter un écosystème numérique souverain** — DNS interne, AC/PKI, identité (LDAP + SSO), relais courriel, bases de données, observabilité, applications — sur sa propre grappe **Proxmox**, à partir d'un **plan déclaratif**.
Set-OPS construit et exploite un **écosystème numérique souverain** sur une grappe
**Proxmox**, à partir d'un **plan déclaratif** : DNS, AC interne, identité (LDAP + SSO),
courriel, bases de données, observabilité, forge, applications web. Logiciel libre
exclusivement ; ni Docker, ni Podman.
Le dépôt est le **moteur** (générique, partageable). Chaque déploiement réel est une **instance** (le plan + l'inventaire d'un hébergeur, dans son propre dépôt). Tu crées la tienne à partir d'un **modèle prêt à déployer** (`exemples/modeles/`).
- **Moteur** (ce dépôt) : rôles Ansible, playbooks, scripts de dérivation, preuves.
- **Écosystème** : un dépôt par locataire (`OPS-<nom>`) — son plan, son inventaire
généré, sa voûte. Un dépôt par site d'hébergement (`SITE-<nom>`) — son underlay, sa
frontière.
- **Plan → inventaire → déploiement** : on édite le plan, le moteur génère l'inventaire et
l'ordre de déploiement, Ansible applique. L'inventaire ne s'édite jamais à la main.
- **Sans IA** : la doc, `make` et le GUI suffisent à l'exploiter.
## Par où entrer — selon ce que tu viens faire
## Démarrage rapide
On n'arrive pas avec un *sujet*, on arrive avec une **situation**. Il y en a six :
| Ta situation | Ta porte |
|---|---|
| « Je veux **monter** mon écosystème sur ma grappe Proxmox » | [`QUICKSTART.md`](QUICKSTART.md) |
| « Je **prête mon matériel** à quelqu'un qui déploiera dessus » | [`docs/preparer-un-site-hebergeur.md`](docs/preparer-un-site-hebergeur.md) |
| « Je vais **implanter un tenant existant sur un site neuf** » | [`docs/implanter-un-tenant-sur-un-site.md`](docs/implanter-un-tenant-sur-un-site.md) |
| « Je viens d'**hériter** d'un écosystème déjà déployé, je dois l'exploiter » | [`wiki/Reprendre-l-écosystème.md`](wiki/Reprendre-l-%C3%A9cosyst%C3%A8me.md) |
| « Je dois **modifier** le moteur » | [`docs/carte-set-ops.md`](docs/carte-set-ops.md) |
| « J'**apprends** le métier » | [`wiki/Home.md`](wiki/Home.md) |
Et si tu veux d'abord savoir *ce que c'est* : continue simplement ci-dessous.
> Le wiki (`wiki/`) est **publié depuis ce dépôt** vers la forge. Une page modifiée dans
> l'interface de la forge est détruite à la publication suivante : on lit là-bas, on écrit ici.
**Souveraineté jusqu'au bout : Set-OPS s'exploite entièrement à la main** — la doc, `make` et le GUI suffisent, **sans aucune IA**. L'outil libère de la dépendance aux géants ; il ne la remplace pas par une dépendance à une IA.
---
## Mission (la vision derrière l'outil)
`Set-OPS` ne se limite plus à l'exploitation : il **définit et construit un écosystème numérique souverain** — une infrastructure interne auto-suffisante, sans dépendance SaaS, décrite en code et pilotée par des registres machine-lisibles qui font office de **source unique de vérité** (`instance/plan/serveurs.yml`, `instance/plan/applications.yml`, `instance/plan/bases-donnees.yml`, `instance/plan/domaines.yml`, `instance/plan/nomenclature.yml`, `docs/dependances-groupes.yml`).
Piliers de l'écosystème :
- **identité** — machines (PKI / certificats) et utilisateurs (annuaire + SSO) ;
- **confiance** — autorité de certification interne (ACME) ;
- **nommage et adressage** — DNS interne et nomenclature dérivable ;
- **données** — bases relationnelles et cache, avec registre des connexions ;
- **communication** — service de courriel souverain (boîtes LDAP, SMTP, IMAP, antispam, DKIM), et le relais des notifications système ;
- **observabilité et supervision** — métriques, journaux, tableaux de bord, supervision active ;
- **applicatif** — services internes (forge, etc.) et couche web.
L'état voulu est **déclaratif et convergent** (appliqué par les groupes Ansible), **souverain** par conception, mais **piloté par l'opérateur** (pas d'auto-remédiation : la boucle n'est pas fermée). Ce n'est pas resté sur le papier : la flotte a été **rasée et remontée depuis zéro** le 2026-08-13, puis deux fois le 2026-09-02, sans échec. Le degré de maturité service par service est dans `docs/catalogue-services.md`. Le template Debian 13 reste la fondation, pas la finalité.
Cadre et règles d'autorité : voir `AGENTS.md` (section « Mission et identité »).
## Le plan : on édite, l'inventaire se génère
`Set-OPS` se pilote par un **plan**, pas par l'édition directe de l'inventaire.
`instance/inventories/<inventaire>/hosts.yml` est **généré** depuis le plan — **ne pas l'éditer à la main**.
```
éditer le PLAN → make instancier (revoir le diff) → make instancier-appliquer → make deployer
```
- **Plan** : `instance/plan/serveurs.yml` (les VM), `instance/plan/applications.yml` (les services et leurs liens), `instance/plan/bases-donnees.yml`, `instance/plan/domaines.yml`, dérivés via `instance/plan/nomenclature.yml`.
- **GUI** (`make inventaire-ui`) : vues **Serveurs** et **Applications** pour éditer, puis « Appliquer le plan » ; la vue **Inventaire** est en lecture seule.
- VMID / IP / VLAN sont **dérivés** de la `fonction` ; les groupes d'une VM sont dérivés des applications qui y tournent.
Guide complet : **`docs/plan-et-generation.md`**.
## Portée transverse
Au-delà des piliers ci-dessus, le dépôt couvre des préoccupations transverses, communes à toutes les VM :
- templates de VM Proxmox (la fondation) ;
- groupes de conformité et durcissement ;
- maintenance ;
- sauvegardes.
## Premier chantier
Template Debian 13 Proxmox :
```text
playbooks/modeles_vm/debian13_proxmox_preparer.yml
playbooks/modeles_vm/debian13_proxmox_verifier.yml
playbooks/modeles_vm/debian13_proxmox_nettoyer.yml
```
## Principe
Le template contient seulement le socle commun. Les services spécialisés sont installés
ensuite sur les clones, par les playbooks de groupes : edge nginx, PostgreSQL et Redis,
identité (OpenLDAP, Keycloak), courriel (Postfix, Dovecot, rspamd), observabilité
(Prometheus, Loki, Grafana), supervision (Icinga), forge (Forgejo), collaboration
(Nextcloud, Collabora), plateforme web. La liste qui fait foi est
[`docs/catalogue-services.md`](docs/catalogue-services.md).
> **Ni MariaDB, ni Docker, ni Podman.** Cette section les a listés jusqu'au 2026-09-06,
> par recopie de la liste de ce que le *template* ne doit pas contenir. Le dépôt n'a pas de
> rôle MariaDB, et **plus aucun rôle n'a besoin de Docker** depuis la réécriture native de
> `serveur_collabora` — qui en était la dernière exception. Le conteneur n'est pas un
> détail d'implémentation ici : c'est un choix fondateur (voir `roles/serveur_collabora/README.md`).
## Exploitation courante
Un `Makefile` fournit les commandes d'exploitation principales.
Afficher l'aide :
Prérequis : une grappe Proxmox et un jeton d'API, un poste Linux avec Python 3, Ansible,
`make` et `git`, une paire de clés SSH.
```bash
make
```
git clone <url-de-Set-OPS> Set-OPS && cd Set-OPS
Valider le dépôt :
# 1. Un écosystème, depuis le modèle public, et la clé de sa voûte
make instance-creer NOM=OPS-MonOrg MODELE=socle
ln -s ../OPS-MonOrg instance
install -m 600 /dev/null ~/.config/setops-vault-ops-monorg # la phrase de passe, une ligne
# renseigner domaine_interne, nomenclature, domaines, placement : docs/setup.md §3
```bash
# 2. Proxmox et le gabarit Debian 13 (une fois par grappe)
make config
make preparer-modele && make verifier-modele && make nettoyer-modele CONFIRMER=true
# 3. Inventaire, VM, déploiement
make instancier && make instancier-appliquer FORCE=1
make creer-vm HOTE=infra-dns-01
make deployer HOTE=infra-dns-01
# 4. Vérifier
make verifier
```
Produire une **preuve de conformité** horodatée (rejoue les preuves du registre des
affirmations → `docs/audit/preuve-<date>.md` ; mode d'emploi : `docs/audit/README.md`) :
Chaque étape, ses fichiers et ses pièges : [`docs/setup.md`](docs/setup.md).
```bash
make prouver
```
## Documentation
Inspecter les inventaires :
```bash
make inventaire
make hote-afficher HOTE=web-frontal-01
```
Ouvrir l'interface locale de gestion d'inventaire :
```bash
make inventaire-ui
```
Planifier une VM passe désormais par le **plan**, pas par l'édition de l'inventaire :
1. déclarer la VM dans `instance/plan/serveurs.yml` (vue **Serveurs** du GUI, ou `make serveurs`) — `fonction`, `etat`, placement ; VMID/IP/VLAN sont dérivés ;
2. déclarer les services qui y tournent dans `instance/plan/applications.yml` (vue **Applications**) ;
3. régénérer l'inventaire :
```bash
make instancier # génère + diff sémantique (que va-t-il changer ?)
make instancier-appliquer # régénère instance/inventories/<inventaire>/hosts.yml
```
Les anciennes commandes `make hote-planifier` / `hote-ajouter` / `hote-groupes` éditaient l'inventaire **directement** ; elles sont **supplantées** par le plan (l'inventaire est généré, ne pas l'éditer à la main).
Chaque groupe opérationnel doit avoir son playbook homonyme dans `playbooks/groupes/`.
La conformité normale des VM passe par ces playbooks de groupes. Les rôles de socle et de durcissement sont appliqués par `serveur_debian` et `serveur_durci`, pas par des playbooks de couches séparés.
Les groupes opérationnels avec des hôtes, comme `serveur_keycloak` ou `client_pki`, sont validés contre `playbooks/groupes/` par les commandes d'inventaire.
Le catalogue des services et intégrations prévus est dans `docs/catalogue-services.md`.
Les dépendances causales entre groupes sont dans `docs/dependances-groupes.yml`.
Le runbook du DNS interne initial est dans `docs/dns-interne.md`.
La nomenclature des noms de VM et des VMID est dans `docs/nomenclature-vm.md`.
Créer un clone depuis le modèle Debian 13 via l'API Proxmox :
```bash
make config
make creer-vm HOTE=web-frontal-01 # VMID/IP/VLAN/passerelle lus dans le plan
make deployer HOTE=web-frontal-01
```
L'hôte doit d'abord être déclaré dans `instance/plan/serveurs.yml` et l'inventaire régénéré (`make instancier-appliquer`) : `creer-vm` ne prend que `HOTE`, le reste est dérivé.
Les valeurs Cloud-Init communes déjà présentes dans le modèle Proxmox sont héritées par les clones.
Préparer le golden template Debian 13 Proxmox :
```bash
make preparer-modele
make verifier-modele
```
Le nettoyage final du template est protégé :
```bash
make nettoyer-modele CONFIRMER=true
```
Déployer ou remettre en conformité une VM Debian clonée depuis le template :
```bash
make deployer HOTE=web-frontal-01
```
Déployer ou remettre en conformité un groupe :
```bash
make deployer-groupe GROUPE=serveur_debian
```
Les déploiements de groupes ciblent automatiquement les hôtes actifs seulement.
| Document | Contenu |
|---|---|
| [`docs/README.md`](docs/README.md) | l'index : par situation, par lecteur |
| [`docs/architecture.md`](docs/architecture.md) | composants, flux de données, réseau, sécurité |
| [`docs/setup.md`](docs/setup.md) | installation et configuration |
| [`docs/usage.md`](docs/usage.md) | conventions, variables, toutes les commandes |
| [`wiki/Home.md`](wiki/Home.md) | le métier, pas à pas |
| [`AGENTS.md`](AGENTS.md) | les règles d'autorité du dépôt |
## Licence
Copyright (C) 2026 Daniel Allaire — Chezlepro / Alliance Boréale.
Set-OPS est un logiciel **libre** : vous pouvez le redistribuer et/ou le modifier selon les
termes de la **GNU Affero General Public License** telle que publiée par la Free Software
Foundation, soit la version 3, soit (à votre choix) toute version ultérieure.
Set-OPS est distribué dans l'espoir qu'il sera utile, mais **SANS AUCUNE GARANTIE** ; sans même
la garantie implicite de QUALITÉ MARCHANDE ou d'ADÉQUATION À UN USAGE PARTICULIER. Voir la GNU
Affero General Public License pour plus de détails.
Le texte intégral fait foi dans le fichier [`LICENSE`](LICENSE) et sur
<https://www.gnu.org/licenses/agpl-3.0.html>.
### Pourquoi l'AGPLv3
Libre **et** copyleft fort. Chacun peut utiliser, exécuter, étudier et modifier Set-OPS —
**y compris commercialement**. En contrepartie, quiconque l'**offre comme service en réseau**
doit mettre à disposition ses modifications sous la même licence. Cela protège la **souveraineté**
(pas de captation propriétaire) **sans jamais interdire l'usage commercial** — cohérent avec le
principe fondateur : *tout est libre*.
### Modèle : libre + services
Set-OPS est **entièrement libre** ; aucune fonctionnalité n'est réservée ni verrouillée. Les
revenus, le cas échéant, proviennent de **services autour** du logiciel, jamais de sa fermeture :
- support / SLA, **hébergement géré**, formation, intégration, développement sur mesure ;
- **certification** d'écosystèmes (label *Souverain / Résilient / Exemplaire*).
Les artisans et coopératives peuvent exploiter Set-OPS pour **leur propre infrastructure sans
redevance** : l'usage interne ne déclenche aucune obligation de partage. Le copyleft AGPL ne vise
que la captation propriétaire (offrir Set-OPS en service fermé sans en partager les évolutions).
Copyright (C) 2026 Daniel Allaire — Chezlepro / Alliance Boréale. Logiciel libre sous
**GNU Affero General Public License v3** ou ultérieure : voir [`LICENSE`](LICENSE). Pourquoi
l'AGPLv3, et le modèle libre + services : [`docs/projet/positionnement.md`](docs/projet/positionnement.md) §6.

View file

@ -1,78 +0,0 @@
# La solution Set-OPS
> **Document historique de décision.** Le parcours opérationnel à jour vit dans
> [`README.md`](README.md) et [`QUICKSTART.md`](QUICKSTART.md) ; l'autorité est
> [`AGENTS.md`](AGENTS.md). Les commandes brutes ci-dessous sont **supplantées** par les
> cibles `make` (`preparer-modele` / `verifier-modele` / `nettoyer-modele`). Conservé pour
> tracer la décision fondatrice.
## Décision
`Set-OPS` est le moteur Ansible global d’exploitation d’écosystèmes numériques souverains.
Le template Debian 13 Proxmox est seulement un chantier dans ce dépôt.
## Ce qu'on fait maintenant
On garde une seule structure simple :
```text
inventories/
playbooks/
roles/
docs/
```
Les rôles sont nommés clairement, sans sous-arborescence complexe.
> Réalité actuelle du dépôt : au-delà de ces quatre dossiers, le moteur comporte aussi
> `scripts/` (plan + GUI), `filter_plugins/`, `exemples/`, `wiki/` et l'instance montée
> (`instance/`). Vue d'ensemble : [`docs/carte-set-ops.md`](docs/carte-set-ops.md).
## Commande principale pour construire le template
Le template se construit avec un **accès SSH par clé dès le départ** (clé injectée par
cloud-init) — pas par mot de passe. La cible opérateur est :
```bash
make preparer-modele
```
Elle exécute `playbooks/modeles_vm/debian13_proxmox_preparer.yml` sur l'inventaire modèle,
après avoir vérifié l'accès SSH par clé et les privilèges sudo du compte `ansible`. Voir
[`AGENTS.md`](AGENTS.md) § SSH pour l'état cible (`PasswordAuthentication no`,
`AuthenticationMethods publickey`).
## Vérification
```bash
make verifier-modele
```
## Nettoyage final avant conversion
```bash
make nettoyer-modele CONFIRMER=true
```
*(Ces deux blocs appelaient `ansible-playbook -i instance/inventories/lab/hosts.yml …`
jusqu'au 2026-09-06. Cet inventaire n'existe pas : le nom est résolu par le `Makefile`,
c'est précisément pourquoi les cibles `make` supplantent les commandes brutes.)*
Ensuite :
```bash
sudo shutdown -h now
```
Puis côté Proxmox :
```bash
qm template VMID
```
## Important
Le pare-feu `nftables` est installé et préparé, mais il n'est pas activé par défaut dans le template.
C'est volontaire pour éviter de couper SSH pendant la construction.

121
docs/README.md Normal file
View file

@ -0,0 +1,121 @@
# Documentation de Set-OPS
> **Pour qui :** tout lecteur qui cherche une page. On entre par sa **situation** ou par son
> **rôle** ; chaque document dit en tête à qui il s'adresse.
## Portes d'entrée
| Document | Contenu |
|---|---|
| [`architecture.md`](architecture.md) | composants, moteur / site / locataire, plan → déploiement, réseau, sécurité (schémas) |
| [`setup.md`](setup.md) | prérequis, instance, voûtes, Proxmox, gabarit, premier déploiement |
| [`usage.md`](usage.md) | conventions, variables, toutes les commandes `make` par famille |
## Par situation
| Ta situation | Ta porte |
|---|---|
| « Je veux **monter** mon écosystème sur ma grappe Proxmox » | [`setup.md`](setup.md) |
| « Je **prête mon matériel** à quelqu'un qui déploiera dessus » | [`hebergement/preparer-un-site-hebergeur.md`](hebergement/preparer-un-site-hebergeur.md) |
| « Je vais **implanter un locataire existant sur un site neuf** » | [`hebergement/implanter-un-tenant-sur-un-site.md`](hebergement/implanter-un-tenant-sur-un-site.md) |
| « Je viens d'**hériter** d'un écosystème déployé, je dois l'exploiter » | [`../wiki/Reprendre-l-écosystème.md`](../wiki/Reprendre-l-%C3%A9cosyst%C3%A8me.md) |
| « Je fais face à une **situation d'exploitation** » | [`exploitation/runbooks-exploitation.md`](exploitation/runbooks-exploitation.md) |
| « Je dois **modifier** le moteur » | [`conception/mecanismes.md`](conception/mecanismes.md), puis `AGENTS.md` |
| « J'**apprends** le métier » | [`../wiki/Home.md`](../wiki/Home.md) |
| « J'**évalue** Set-OPS » | [`projet/pouvoirs-set-ops.md`](projet/pouvoirs-set-ops.md), [`projet/ecosysteme-chezlepro.md`](projet/ecosysteme-chezlepro.md) |
## Par lecteur
### `conception/` — le mainteneur : comment le moteur est fait
| Document | Sujet |
|---|---|
| `mecanismes.md` | les mécanismes transverses et où ils vivent ; maturité, dette, discipline |
| `plan-et-generation.md` | le plan, ses registres, la génération de l'inventaire |
| `meta-classe.md` | pourquoi on écrit *un* plan et pas *un* inventaire |
| `conception-contextes.md` | site et locataire : deux classes, un contrat |
| `multi-instances.md` | le moteur et ses écosystèmes, le seed `index` |
| `filiation-emancipation.md` | ce que le site prête au locataire, et comment il s'en détache |
| `depots-prives-conception.md` | chaque runner ne lit que ses dépôts |
| `nomenclature-vm.md` | noms de VM et VMID |
| `dimensionnement-ressources.md` | RAM, CPU, disque dérivés des empreintes |
| `intrants-communs.md` | valeurs partagées par tout l'écosystème |
| `intrants-base-gui-conception.md` | le panneau « Intrants de base » du GUI |
| `bindings-conception.md` | relations service → service et service → base |
| `integrations-vm.md` | les intégrations `client_*` posées sur toutes les VM |
| `flux-conception.md` | déclarer un flux réseau dans `meta/flux.yml` |
| `registre-flux.md` | la matrice d'audit des flux (**générée** par `make flux`) |
| `schemas-flux.md` | la liste des pages publiées et ce qu'elles montrent |
| `sdn-evpn.md` | le réseau overlay : zones EVPN, VNets |
| `frontiere-opnsense.md` | la bordure nord/sud |
| `frontiere-physique-virtuel.md` | physique et virtuel : qui possède quoi, et où ça vit |
| `dns-interne.md` | DNS interne et public, deux sites (schémas) |
| `identite-sso.md` | les deux couches de l'identité |
| `authentification.md` | la doctrine d'authentification, rôle par rôle |
| `courriel-conception.md` | le service de courriel |
| `supervision-conception.md` | les verdicts, vers Icinga |
| `metriques-conception.md` | les séries, vers Prometheus et Grafana |
### `exploitation/` — l'exploitant : faire tourner un écosystème
| Document | Sujet |
|---|---|
| `runbooks-exploitation.md` | que faire, situation par situation |
| `devis-services.md` | ce qui tourne correspond-il à ce qui est déclaré ? |
| `vm-lifecycle.md` | ce qu'une VM traverse, de la création à la destruction |
| `procedure-template-debian13-proxmox.md` | fabriquer le gabarit d'or |
| `gabarit-debian13-justification.md` | ce que le gabarit contient, et pourquoi |
| `config-proxmox.md` | raccorder le moteur à la grappe |
| `acces-administration.md` | le tunnel nominatif des administrateurs : par où, avec quelle clé |
| `autorisation.md` | habilitations ; §6 : le runbook de reprise |
| `sortir-les-cles-du-poste.md` | les clés et les voûtes hors du poste |
| `theme-forgejo-hors-flotte.md` | le thème Alliance Boréale sur une forge hors flotte |
### `hebergement/` — l'hébergeur et le locataire
| Document | Sujet |
|---|---|
| `preparer-un-site-hebergeur.md` | mettre son matériel à disposition |
| `implanter-un-tenant-sur-un-site.md` | le jour de l'intervention sur un site neuf |
| `hebergeur-exploitation.md` | les services que l'hébergeur se doit à lui-même |
| `migration-tenant.md` | déplacer un locataire d'un hébergeur à un autre |
| `remise-au-client.md` | remettre un écosystème à qui en est responsable |
| `responsabilites-locataire-hebergeur.md` | qui répond de quoi |
### `projet/` — décisions, maturité, positionnement
| Document | Sujet |
|---|---|
| `decisions-architecture.md` | les décisions en vigueur et renversées, et ce qui les garde |
| `catalogue-services.md` | ce que le moteur sait déployer, et sa maturité |
| `positionnement.md` | ce qui reste maison, ce qui s'adopte, et à quels seuils ; la licence |
| `pouvoirs-set-ops.md` | ce que le moteur sait faire, et ce qu'il ne sait pas |
| `ecosysteme-chezlepro.md` | un écosystème réel, pour le lecteur externe |
| `MISE-A-JOUR-CODEX-CLAUDE.md` | pour l'agent IA qui reprend le dépôt |
## Registres et preuves
| Fichier | Rôle |
|---|---|
| `couches-deploiement.yml` | les couches de déploiement — lu par `scripts/orchestrer.py` |
| `dependances-groupes.yml` | le graphe des dépendances entre groupes — lu par l'orchestrateur et les preuves |
| `runbooks-construction.yml` | les runbooks de construction — lu par `scripts/runbooks.py` |
| `audit/affirmations.md` | le registre des affirmations que `make prouver` rejoue |
| `audit/preuve-<date>.md` | les rapports de preuve datés |
Le wiki (`wiki/`) est publié depuis ce dépôt vers la forge (`make wiki-publier`) : on y lit,
on écrit ici. L'autorité est `AGENTS.md` ; l'histoire, `CHANGELOG.md`.
## Le dépôt en chiffres
> Ces valeurs sont **mesurées** : **P48** les recompte et refuse tout écart.
| Ce qu'on compte | Combien | Comment on le mesure |
|---|---|---|
| rôles | 70 | `roles/*/` |
| README de rôles | 70 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
| documents | 51 | `docs/**/*.md`, hors `audit/` |
| pièces d'audit | 53 | `docs/audit/*` |
| unités de wiki | 27 | `wiki/*.md` |
| décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |

View file

@ -1,71 +0,0 @@
# Architecture Set-OPS
> **Pour qui :** le **mainteneur** — le survol du modèle. À lire avant `plan-et-generation.md`.
Set-OPS définit et construit l'écosystème numérique souverain. Il se
pilote par un **plan** : l'inventaire Ansible (`instance/inventories/<inventaire>/hosts.yml`)
est **généré** depuis le plan, pas édité à la main.
- **Par où commencer + catalogue des mécanismes transverses : `docs/carte-set-ops.md`.**
- Modèle, registres, commandes et flux de travail : **`docs/plan-et-generation.md`**.
- Règles d'autorité : **`AGENTS.md`** (sections « Mission et identité » et « Le plan et la génération de l'inventaire »).
## Entités du plan
```text
serveur (VM) instance/plan/serveurs.yml fonction, état, placement, intégrations
application instance/plan/applications.yml groupe (rôle), hôte, port, requiert, expose
base instance/plan/bases-donnees.yml serveur de BD, base, propriétaire, secret (Vault), portée
domaine (DNS) instance/plan/domaines.yml zone publique, autorité, edge
nomenclature instance/plan/nomenclature.yml fonctions -> VMID / VLAN / IP (dérivés)
```
L'**application** est l'entité pivot ; le **groupe** Ansible n'est qu'une capacité
(le rôle appliqué). VMID / IP / VLAN et les appartenances de groupes sont **dérivés**.
## Rôles Ansible
Les rôles actifs sont conservés directement sous `roles/`.
Les sous-arborescences catégorielles de rôles ne doivent pas dupliquer un rôle actif. Un nouveau rôle doit être ajouté seulement lorsqu'un besoin opérationnel réel existe.
Les valeurs communes aux rôles doivent rester dans les defaults des rôles quand elles sont valables pour tous les environnements. Les inventaires ne doivent contenir que les politiques propres à leur contexte.
## Groupes et playbooks
Les groupes opérationnels de l'inventaire doivent correspondre à un playbook homonyme :
```text
instance/inventories/<inventaire>/hosts.yml
serveur_debian
playbooks/groupes/serveur_debian.yml
```
Cette relation rend l'exploitation lisible :
```text
appartenance à un groupe
→ playbook correspondant
→ état voulu appliqué par Ansible
```
Les groupes structurels ou vides peuvent exister, mais un groupe assigné à une VM de production doit avoir un playbook s'il exprime une intention de configuration.
## Cycle de vie VM
Le cycle de vie des VM est documenté dans :
```text
docs/vm-lifecycle.md
```
## Intégrations transversales
Elles ne sont plus « futures » : `client_pki`, `client_backup`, `client_metrique`,
`client_journal`, `client_smtp`, `client_resolveur` et `client_artefacts` sont déployés sur
la flotte. Elles sont documentées dans :
```text
docs/integrations-vm.md
```

180
docs/architecture.md Normal file
View file

@ -0,0 +1,180 @@
# Architecture
> **Pour qui :** quiconque doit comprendre comment Set-OPS est construit — avant de
> l'installer, de l'exploiter ou de le modifier. Le détail de chaque mécanisme est dans
> `docs/conception/` ; l'index complet dans [`README.md`](README.md).
Set-OPS est un **moteur** Ansible et Python. Il lit un **plan** déclaratif, en dérive un
inventaire et des politiques, et les applique à des VM Debian 13 clonées sur une grappe
Proxmox. Le moteur est générique ; chaque écosystème réel vit dans **son propre dépôt**.
## Composants
```mermaid
flowchart LR
subgraph POSTE["Poste de l'exploitant (ou runner)"]
MOT["Set-OPS<br/>moteur : roles/, playbooks/, scripts/"]
INST["instance/ → ../OPS-X<br/>le locataire actif"]
UND["underlay.yml → ../SITE-Y/underlay.yml<br/>le site actif"]
CLE["~/.config/setops-vault-*<br/>une clé par voûte"]
end
subgraph SITE["Site d'hébergement"]
PVE["Grappe Proxmox<br/>SDN EVPN, pare-feu est-ouest"]
OPN["Frontière OPNsense<br/>nord/sud"]
RUN["site-ops-01<br/>runner du site"]
FORGE["Forge du site<br/>le génome"]
VMS["VM des locataires<br/>et du site"]
end
MOT -- "lit" --> INST & UND
MOT -- "API Proxmox : clone, SDN, pare-feu" --> PVE
MOT -- "API OPNsense : règles, routes" --> OPN
MOT -- "SSH + Ansible" --> VMS
POSTE -- "make publier / genome-pousser" --> FORGE
RUN -- "tire" --> FORGE
RUN -- "SSH + Ansible" --> VMS
PVE --- VMS
```
| Composant | Ce que c'est | Où |
|---|---|---|
| Moteur | rôles, playbooks, scripts de dérivation, preuves | ce dépôt |
| Locataire (tenant) | un écosystème : son plan, son inventaire généré, sa voûte | dépôt `OPS-<nom>`, monté par `instance/` |
| Site | l'hébergeur : matériel, underlay, frontière, services prêtés aux locataires | dépôt `SITE-<nom>`, désigné par `underlay.yml` |
| Voûtes | secrets chiffrés `ansible-vault`, une par dépôt, une clé par voûte | `group_vars/all/vault.yml` ; clés hors dépôt |
| Runner du site | machine du site qui déploie à la place du poste | `site-ops-01`, compte `setops`, `/opt/setops` |
| Forge du site | Forgejo qui porte le génome (moteur + plans) que les runners tirent | VM du site |
## Moteur, site, locataire
```mermaid
flowchart TB
M["Set-OPS (moteur)"]
S["SITE-Y<br/>underlay.yml · plan/ · opnsense.yml"]
T1["OPS-A<br/>plan/ · inventories/"]
T2["OPS-B<br/>plan/ · inventories/"]
M -. "monte un site" .-> S
M -. "monte un locataire" .-> T1 & T2
S -- "contrat du site → fiche-site.yml" --> T1 & T2
T1 & T2 -- "face réseau publiée" --> S
```
- **Deux classes d'écosystème, un tronc commun** : `scripts/contexte.py` est le seul endroit
qui lit les indices (`instance/`, `underlay.yml`, `SETOPS_*`, dossiers frères) et rend un
`Site` ou un `Locataire`. Conception : `docs/conception/conception-contextes.md`.
- **Le site lit la face réseau des locataires, le locataire lit la fiche du site.** Le
contrat (`make site-intrants`) dérive ce que le site expose : résolveur, cache de paquets,
forge, adresses publiques, serveurs DNS publics. `make fiches-site-deposer` le dépose chez
chaque locataire (`fiche-site.yml`) ; `make site-intrants-verifier` juge l'écart.
- **Multi-instance par convention** : un dépôt par écosystème, découverts comme dossiers
frères. Tout l'adressage dérive d'un seed `index` par écosystème ; P21 refuse une
collision. Détail : `docs/conception/multi-instances.md`.
- **Le site prête trois services** à un locataire jeune — paquets (`serveur_cache_site`),
génome (`serveur_forge_site`), noms (`serveur_resolveur_site`) — jusqu'à ce qu'il ait les
siens. Filiation et émancipation : `docs/conception/filiation-emancipation.md`.
## Du plan au déploiement
```mermaid
flowchart LR
PLAN["plan/<br/>serveurs · applications · bases-donnees<br/>domaines · nomenclature"] --> INS["make instancier<br/>scripts/instancier.py<br/>inventory_rules.py"]
INS -- "diff sémantique" --> APP["make instancier-appliquer"]
APP --> HOSTS["inventories/INVENTAIRE/hosts.yml<br/>GÉNÉRÉ"]
COUCHES["docs/couches-deploiement.yml<br/>docs/dependances-groupes.yml"] --> ORCH["make site<br/>scripts/orchestrer.py"]
ORCH --> SITEYML["playbooks/site.yml<br/>GÉNÉRÉ"]
HOSTS & SITEYML --> ANS["make deployer · deployer-tout<br/>ansible-playbook"]
VAULT["vault.yml<br/>ANSIBLE_VAULT_IDENTITY_LIST"] --> ANS
ANS --> VM["VM : groupes → playbooks/groupes/GROUPE.yml → rôles"]
```
1. **On édite le plan, jamais l'inventaire.** VMID, IP, VLAN, FQDN et appartenances aux
groupes sont dérivés de la `fonction` d'une VM et des applications qui y tournent.
2. **L'inventaire est généré** et comparé à l'existant avant d'être écrit.
3. **L'ordre de déploiement est généré** depuis les couches et le graphe des dépendances
entre groupes.
4. **Un groupe = un playbook homonyme** (`playbooks/groupes/<groupe>.yml`) qui applique ses
rôles. Le socle (`serveur_debian`) et le durcissement (`serveur_durci`) passent en premier.
Les registres du plan :
| Registre | Entité | Champs clés |
|---|---|---|
| `plan/serveurs.yml` | VM | `fonction`, `etat` (`planifie` → `actif`), placement |
| `plan/applications.yml` | service (entité pivot) | `groupe` (rôle), `hote`, `port`, `liens`, `expose` |
| `plan/bases-donnees.yml` | base | serveur, propriétaire, secret en voûte, `consommateur`, `portee` |
| `plan/domaines.yml` | zone DNS | `autorite`, `edge`, `dnssec`, `enregistrements`, `resolution_interne` |
| `plan/nomenclature.yml` | dérivation | `index` (seed), fonctions → VMID / VLAN / IP |
Détail : `docs/conception/plan-et-generation.md`, `docs/conception/meta-classe.md`.
## Ce qu'un rôle déclare, ce que le moteur dérive
Chaque rôle porte ses déclarations dans `roles/<rôle>/meta/`. Le moteur les agrège ; aucune
liste n'est tenue à la main ailleurs.
| Déclaration | Ce que le moteur en dérive | Doc |
|---|---|---|
| `meta/flux.yml` | nftables de l'hôte, pare-feu Proxmox, frontière OPNsense, registre des flux | `conception/flux-conception.md` |
| `meta/liens.yml` | variables de connexion service → service | `conception/bindings-conception.md` |
| `meta/empreinte.yml` | RAM, CPU, disque de la VM | `conception/dimensionnement-ressources.md` |
| `meta/supervision.yml`, `meta/metriques.yml` | services Icinga, cibles Prometheus, tableaux | `conception/supervision-conception.md` |
| `meta/authentification.yml` | portée et mécanisme d'authentification du rôle, gardés par P29 | `conception/authentification.md` |
| `meta/acces.yml` | rôles d'accès projetés dans Keycloak | `exploitation/autorisation.md` |
| `meta/integration.yml` | intégrations universelles (`client_*`) posées sur chaque VM | `conception/integrations-vm.md` |
## Réseau
```mermaid
flowchart TB
NET(("Internet")) --> OPN["Frontière OPNsense<br/>nord/sud · seul équipement L3 de bordure"]
OPN --> SDN["SDN EVPN sur les hyperviseurs<br/>une zone (VRF) par écosystème · passerelles anycast"]
SDN --> PFW["Pare-feu Proxmox<br/>est-ouest, par VM"]
PFW --> NFT["nftables de l'hôte"]
NFT --> SVC["service"]
FLUX["roles/*/meta/flux.yml"] -. "dérive" .-> OPN & PFW & NFT
```
- **Underlay** (`underlay.yml` du site) : la fabric physique — gestion, iSCSI, Ceph, ponts.
`make underlay-plan` le confronte au réel.
- **Overlay** : un écosystème = une zone EVPN et ses VNets, nommés depuis le seed
(`make sdn-plan`). Détail : `docs/conception/sdn-evpn.md`.
- **Quatre couches de filtrage, une seule source** : les flux déclarés par les rôles.
`pair: externe` devient la politique de bordure. Détail :
`docs/conception/frontiere-opnsense.md`, `docs/conception/registre-flux.md`.
- **Deux sites reliés** par un tunnel WireGuard (`opnsense_liens_sites`) : réplication du DNS
public, administration du site pair. DNS : `docs/conception/dns-interne.md`.
## Déclaré, dérivé, mesuré
```mermaid
flowchart LR
D["Déclaré<br/>plan · meta/ · cartes"] --> V["Dérivé<br/>devis : scripts/devis_*.py"]
V --> P["make SUJET-plan<br/>écart, aucune écriture"]
P --> A["make SUJET-appliquer CONFIRMER=true<br/>réconcilie"]
R["Réel<br/>API Proxmox · OPNsense · VM"] --> P
D --> PR["make prouver / verifier<br/>preuves statiques : lisent le dépôt"]
```
- **Les preuves** (`make verifier`, `make prouver`) lisent le dépôt seulement : cohérence du
plan, des rôles, de la doc. Registre : `docs/audit/affirmations.md`.
- **Les devis** lisent le système en marche et le comparent à ce que le plan dérive. Ils
n'écrivent rien ; la cible `-appliquer` correspondante réconcilie, sous `CONFIRMER=true`.
Détail : `docs/exploitation/devis-services.md`.
## Sécurité
| Sujet | Mécanisme | Doc |
|---|---|---|
| Secrets | une voûte par dépôt, une clé par voûte, `scripts/voutes.py` construit `ANSIBLE_VAULT_IDENTITY_LIST` | `exploitation/sortir-les-cles-du-poste.md` |
| Confiance machine | AC interne step-ca (ACME), certificats par `client_pki` | `conception/integrations-vm.md` |
| Identité | OpenLDAP source unique, Keycloak fédéré, `oauth2-proxy` devant les services sans OIDC | `conception/identite-sso.md`, `conception/authentification.md` |
| Habilitations | amorcées par `amorcage_acces`, jamais réconciliées | `exploitation/autorisation.md` |
| Administration | tunnel WireGuard nominatif `admins`, rebond par la frontière | `exploitation/acces-administration.md` |
| Transit | chaque flux déclare son `chiffrement` ; le registre en fait la synthèse | `conception/flux-conception.md`, `conception/registre-flux.md` |
## Invariants
- Logiciel libre exclusivement ; ni Docker, ni Podman.
- L'inventaire et `playbooks/site.yml` sont générés : ne pas les éditer.
- Aucun nom d'inventaire codé en dur : `<inventaire>` est résolu par `ORDRE_INVENTAIRE`.
- Le moteur s'exploite sans IA : la doc, `make` et le GUI suffisent.
- L'autorité est `AGENTS.md`.

View file

@ -41,9 +41,9 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
| AFF-007 | « Inspecter … `make hote-afficher HOTE=web-frontal-01` » (README:98) | procédurale | `make hote-afficher HOTE=web-frontal-01` | 🟡 | La cible existe et fonctionne, mais l'hôte d'exemple `web-frontal-01` n'existe pas dans l'instance courante → l'exemple littéral échoue (exemple générique, pas garanti par instance). |
| AFF-008 | « Chaque groupe opérationnel doit avoir son playbook homonyme dans `playbooks/groupes/` » (README:120 ; AGENTS:316) | structurelle | `python3 scripts/inventory_host.py --inventaire instance/inventories/principal/hosts.yml verifier-playbooks --dossier-playbooks playbooks/groupes` | ✅ | rc=0. Les 30 groupes opérationnels ont leur playbook. Seuls `hotes_actifs`, `hotes_planifies`, `modeles_vm` (groupes méta/état) n'en ont pas — exclus par conception. |
| AFF-009 | « Les dépendances causales entre groupes sont dans `docs/dependances-groupes.yml` » (README:128 ; AGENTS:413) | structurelle | `python3 scripts/inventory_host.py … verifier-dependances …` | ✅ | rc=0. Aucune entrée ne référence un rôle/groupe inexistant. |
| AFF-010 | « Le runbook du DNS interne initial est dans `docs/dns-interne.md` » (README:130) | structurelle | `test -f docs/dns-interne.md` | ✅ | Fichier présent. |
| AFF-011 | « La nomenclature … est dans `docs/nomenclature-vm.md` » (README:132) | structurelle | `test -f docs/nomenclature-vm.md` | ✅ | Fichier présent. |
| AFF-012 | « Le catalogue des services … `docs/catalogue-services.md` » (README:126) | structurelle | `test -f docs/catalogue-services.md` | ✅ | Fichier présent. |
| AFF-010 | « Le runbook du DNS interne initial est dans `docs/conception/dns-interne.md` » (README:130) | structurelle | `test -f docs/conception/dns-interne.md` | ✅ | Fichier présent. |
| AFF-011 | « La nomenclature … est dans `docs/conception/nomenclature-vm.md` » (README:132) | structurelle | `test -f docs/conception/nomenclature-vm.md` | ✅ | Fichier présent. |
| AFF-012 | « Le catalogue des services … `docs/projet/catalogue-services.md` » (README:126) | structurelle | `test -f docs/projet/catalogue-services.md` | ✅ | Fichier présent. |
| AFF-013 | « `make creer-vm HOTE=… ` VMID/IP/VLAN/passerelle lus dans le plan » (README:138) | fonctionnelle | `python3 scripts/inventory_host.py … parametres-proxmox --hote <h>` | 🟡 | La dérivation existe (sous-commande `parametres-proxmox`), mais l'exécution réelle touche Proxmox → non testable localement (hors production). Mécanique de dérivation seule vérifiable. |
| AFF-014 | « Les déploiements de groupes ciblent automatiquement les hôtes actifs seulement » (README:171) | fonctionnelle | lecture `Makefile:261` : `--limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)'` | ✅ | Intersection `groupe ∩ hotes_actifs` codée dans la cible `appliquer`. |
| AFF-015 | « Set-OPS est un logiciel libre … AGPLv3 … texte intégral dans `LICENSE` » (README:177-186) | structurelle | `test -f LICENSE` | ✅ | `LICENSE` présent. |
@ -52,6 +52,9 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
## 2. QUICKSTART.md
> **Retiré le 2026-10-15**, fondu dans `README.md` (démarrage rapide) et `docs/setup.md`.
> Les affirmations ci-dessous gardent leurs références d'origine : c'est la trace de l'audit.
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-020 | « Les modèles sont dans `exemples/modeles/` … Choisis : `socle`, `presence-web`, `forge`, `identite`, `observabilite`, `integral` » (QUICKSTART:25-26) | structurelle | `ls exemples/modeles/` | ✅ | Le dossier public ne contient que `socle` (+ `README.md`). Les 5 autres modèles sont dans un dépôt **privé** `Set-OPS-modeles` (correctement expliqué dans `exemples/modeles/README.md`, mais **pas** dans QUICKSTART). — **rejugé le 2026-08-08 : RÉSOLU.** QUICKSTART §2 annonce désormais **un seul modèle générique `socle`** et renvoie les modèles assemblés au dépôt privé — la promesse s'est alignée sur la décision, pas l'inverse |
@ -94,6 +97,9 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
## 5. SOLUTION.md
> **Retiré le 2026-10-15** : document historique, déjà supplanté (AFF-060/061). La décision
> fondatrice reste dans l'histoire du dépôt ; le parcours du gabarit est dans `docs/setup.md` §6.
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-060 | « On garde une seule structure : `inventories/ playbooks/ roles/ docs/` » (SOLUTION:11-18) | structurelle | `ls` racine | 🟡 | Réel : aussi `scripts/`, `filter_plugins/`, `exemples/`, `wiki/`, `instance/`. Description obsolète (antérieure au plan/GUI). |
@ -107,7 +113,7 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-070 | « `make site-verifier` valide la cohérence couches/graphe » (Makefile aide:133) | fonctionnelle | `python3 scripts/orchestrer.py verifier` | ✅ | « Orchestration cohérente : 30 groupes classés, aucun cycle, aucune arête en arrière. » |
| AFF-071 | « `make flux` → `docs/registre-flux.md` + `instance/flux-genere/*.nft` » (Makefile aide:146) | fonctionnelle | `python3 scripts/resoudre_flux.py verifier` ; en-tête de `docs/registre-flux.md` | ✅ | « Flux cohérents : 29 rôles, 63 flux ». Le registre porte l'en-tête « GÉNÉRÉ par `scripts/resoudre_flux.py` ». |
| AFF-071 | « `make flux` → `docs/conception/registre-flux.md` + `instance/flux-genere/*.nft` » (Makefile aide:146) | fonctionnelle | `python3 scripts/resoudre_flux.py verifier` ; en-tête de `docs/conception/registre-flux.md` | ✅ | « Flux cohérents : 29 rôles, 63 flux ». Le registre porte l'en-tête « GÉNÉRÉ par `scripts/resoudre_flux.py` ». |
| AFF-072 | « `make deployer-tout` — déploiement orchestré de la flotte ; garde `CONFIRMER` » (Makefile aide:138) | fonctionnelle | lecture Makefile:352-357 | 🟡 | Garde `CONFIRMER=true` vérifiée (refuse sinon, rc=2). Déploiement réel hors production → non exécuté. |
| AFF-073 | « `make wiki-publier` … refuse sans `WIKI_REMOTE` » (Makefile aide:151) | fonctionnelle | lecture Makefile:320-326 | ✅ | Garde présente : refus + message d'usage si `WIKI_REMOTE` vide. |
| AFF-074 | « `make devis-reseau` — dérivé des nomenclatures » (Makefile:309) | fonctionnelle | `python3 scripts/devis_reseau.py` | 🟡 | Dérive des instances fédérées `../*/plan/nomenclature.yml` (schéma `ip-miroir`) ; sortie dépend du voisinage de dépôts → vérifiable seulement dans un contexte fédéré. |
@ -119,12 +125,12 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-080 | « `make help` — liste des cibles » (docs/config-proxmox.md:114) | procédurale | `grep -E '^help:' Makefile` | ✅ | Aucune cible `help`. La cible réelle est `aide` (défaut). Commande citée inexistante. — **rejugé le 2026-08-08 : RÉSOLU.** plus aucune occurrence de `make help` dans le dépôt ; la cible s'appelle `aide`, et les **85** cibles portent leur texte d'aide (P31) |
| AFF-081 | « `make syntax-template` » (docs/modeles_vm/debian13-proxmox.md:77) | procédurale | `grep syntax-template Makefile` | ✅ | Aucune cible `syntax-template`. Réel : `syntaxe-modele`. Nom périmé. — **rejugé le 2026-08-08 : RÉSOLU.** `syntax-template` n'apparaît plus nulle part hors de ce registre ; la cible est `syntaxe-modele` |
| AFF-082 | « `make syntax-template` » (docs/MISE-A-JOUR-CODEX-CLAUDE.md:146) | procédurale | idem | ✅ | Même nom périmé, second emplacement. — **rejugé le 2026-08-08 : RÉSOLU.** idem AFF-081 |
| AFF-080 | « `make help` — liste des cibles » (docs/exploitation/config-proxmox.md:114) | procédurale | `grep -E '^help:' Makefile` | ✅ | Aucune cible `help`. La cible réelle est `aide` (défaut). Commande citée inexistante. — **rejugé le 2026-08-08 : RÉSOLU.** plus aucune occurrence de `make help` dans le dépôt ; la cible s'appelle `aide`, et les **85** cibles portent leur texte d'aide (P31) |
| AFF-081 | « `make syntax-template` » (docs/exploitation/gabarit-debian13-justification.md:77) | procédurale | `grep syntax-template Makefile` | ✅ | Aucune cible `syntax-template`. Réel : `syntaxe-modele`. Nom périmé. — **rejugé le 2026-08-08 : RÉSOLU.** `syntax-template` n'apparaît plus nulle part hors de ce registre ; la cible est `syntaxe-modele` |
| AFF-082 | « `make syntax-template` » (docs/projet/MISE-A-JOUR-CODEX-CLAUDE.md:146) | procédurale | idem | ✅ | Même nom périmé, second emplacement. — **rejugé le 2026-08-08 : RÉSOLU.** idem AFF-081 |
| AFF-083 | Tous les docs cités par README/QUICKSTART/AGENTS existent (`plan-et-generation`, `positionnement`, `bindings-conception`, `flux-conception`, `couches-deploiement.yml`, `registre-flux.md`, `carte-set-ops`, `runbooks-exploitation`, `vm-lifecycle`, `procedure-template-…`, `dns-interne`, `nomenclature-vm`, `config-proxmox`, `catalogue-services`, `dimensionnement-ressources`, `exemples/vault.exemple.yml`, `exemples/instance.exemple.yml`) | structurelle | `test -f` sur chaque chemin | ✅ | 17/17 présents. |
| AFF-084 | « `docs/dependances-groupes.yml` — chaque groupe existant y figure, aucune entrée ne référence un rôle supprimé » (AGENTS:345,413) | structurelle | `verifier-dependances` (AFF-009) | ✅ | Validé ; graphe sans cycle, tri topologique réussi (via `orchestrer verifier`). |
| AFF-097 | Chemins d'inventaire `lab/` codés en dur pour la config/voûte **Proxmox** et le template, alors que `make config`/`cloner-vm` résolvent `lab > principal > production` (modèle `socle` = `production/`) | procédurale | `grep -rn 'inventories/lab/group_vars' docs playbooks` ; `scripts/config_proxmox.py:31` | 🟡 | **Découvert en Phase 3 (lot A).** Reste `lab/` codé en dur dans `docs/vm-lifecycle.md:171`, `docs/procedure-template-debian13-proxmox.md:741,747`, `docs/modeles_vm/debian13-proxmox.md:89`, `playbooks/proxmox/README.md:31,43,49,50`, `playbooks/proxmox/cloner_vm_debian.yml:45` (message). Non bloquant (l'outillage résout), mais trompeur pour le socle. **Différé** au lot « voûte Proxmox » (couplé à AFF-098). |
| AFF-097 | Chemins d'inventaire `lab/` codés en dur pour la config/voûte **Proxmox** et le template, alors que `make config`/`cloner-vm` résolvent `lab > principal > production` (modèle `socle` = `production/`) | procédurale | `grep -rn 'inventories/lab/group_vars' docs playbooks` ; `scripts/config_proxmox.py:31` | 🟡 | **Découvert en Phase 3 (lot A).** Reste `lab/` codé en dur dans `docs/exploitation/vm-lifecycle.md:171`, `docs/exploitation/procedure-template-debian13-proxmox.md:741,747`, `docs/exploitation/gabarit-debian13-justification.md:89`, `playbooks/proxmox/README.md:31,43,49,50`, `playbooks/proxmox/cloner_vm_debian.yml:45` (message). Non bloquant (l'outillage résout), mais trompeur pour le socle. **Différé** au lot « voûte Proxmox » (couplé à AFF-098). |
| AFF-099 | Le modèle public `socle` livre un `plan/domaines.yml` **invalide** : `exemple.internal` en `autorite: interne`, rejeté par le validateur (`auto-heberge`/`delegue`/`primaire-cache` attendus) | fonctionnelle | `SETOPS_INSTANCE=exemples/modeles/socle python3 scripts/domaines.py verifier` | ✅ | **Découvert Phase 5.** Un opérateur suivant QUICKSTART bute à la validation (étape 9 / `make inventaire-verifier`). `interne` = valeur périmée (déjà corrigée ailleurs en `auto-heberge`, cf. CHANGELOG 2026-07-07). — **rejugé le 2026-08-08 : RÉSOLU.** `SETOPS_INSTANCE=exemples/modeles/socle python3 scripts/domaines.py verifier` → « Registre des domaines valide. » (gardé par P15) |
| AFF-100 | `instancier.py`/`inventory_gui.py` résolvent `principal > production` mais **retombent sur `principal`** quand aucun `hosts.yml` n'existe encore ; le socle est en `production/` → 1ʳᵉ génération dans `principal/`, à côté des `group_vars` restés en `production/` (split-brain) | fonctionnelle | parcours QUICKSTART hors-ligne sur une copie du socle (`generer`→`appliquer`) | ✅ | **Découvert Phase 5.** `config_proxmox.py` détecte le répertoire existant (→ `production/`) ; `instancier`/GUI non → incohérence. QUICKSTART étapes 6-9 cassées pour le socle. — **rejugé le 2026-08-08 : RÉSOLU.** parcours hors-ligne sur copie du socle : `instancier generer` écrit dans `inventories/production/hosts.genere.yml` — plus de repli sur `principal` |
| AFF-098 | Contradiction fonctionnelle voûte Proxmox : `make config` écrit le token dans la voûte unifiée `all/vault.yml` (et `config-proxmox.md` dit de **supprimer** `proxmox.vault.yml`), mais le clonage `cloner_vm_debian.yml` (lancé `-i localhost,`) ne charge le secret **que** depuis `proxmox.vault.yml`/`proxmox.local.yml` ou les variables d'env — **jamais** `all/vault.yml` | fonctionnelle | lecture `playbooks/proxmox/cloner_vm_debian.yml:11-12,51-64` vs `scripts/config_proxmox.py:35` | ✅ | **Découvert en Phase 3 (lot B).** Un opérateur qui suit la voûte unifiée met le token dans `all/vault.yml` → `make creer-vm` échoue l'assert (`proxmox_api_token_secret` vide), sauf variables d'env ou `proxmox.vault.yml` conservé. **Traitement B (code)** : faire lire `all/vault.yml` par le clonage, ou faire écrire `proxmox.vault.yml` par `make config`. À arbitrer dans le lot « voûte Proxmox ». — **rejugé le 2026-08-08 : RÉSOLU.** `cloner_vm_debian.yml` et `config_proxmox.py` disent tous deux « voûte UNIQUE de l'instance » ; `config-proxmox.md` §« `proxmox.vault.yml` n'est plus lue (retirée le 2026-08-03) » |
@ -158,18 +164,18 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-101 | « Tout l'adressage dérive du seul seed `index` ; aucune adresse n'est stockée » (`docs/sdn-evpn.md` §2 ; wiki *Multi-instance*) | structurelle | `python3 scripts/prouver.py` → **P20** | ✅ | Aucun `supernet`/`sous_reseau`/`passerelle`/`vlan` stocké dans une nomenclature. |
| AFF-102 | « Aucune paire d'instances fédérées ne partage un `index` » (`docs/multi-instances.md`) | fonctionnelle | **P21** (`scripts/instances.py --verifier`) | ✅ | 3 instances, aucune collision. Le garde-fou refuse la création en doublon. |
| AFF-101 | « Tout l'adressage dérive du seul seed `index` ; aucune adresse n'est stockée » (`docs/conception/sdn-evpn.md` §2 ; wiki *Multi-instance*) | structurelle | `python3 scripts/prouver.py` → **P20** | ✅ | Aucun `supernet`/`sous_reseau`/`passerelle`/`vlan` stocké dans une nomenclature. |
| AFF-102 | « Aucune paire d'instances fédérées ne partage un `index` » (`docs/conception/multi-instances.md`) | fonctionnelle | **P21** (`scripts/instances.py --verifier`) | ✅ | 3 instances, aucune collision. Le garde-fou refuse la création en doublon. |
| AFF-103 | « L'underlay ne chevauche aucun supernet tenant et reste sous la plage VLAN tenant » (`underlay.yml.example` ; wiki) | structurelle | **P23** (`scripts/underlay.py --verifier`) | ✅ | Sauté si aucun `underlay.yml` monté (dépôt public nu). |
| AFF-104 | « La frontière déclare toujours une source d'administration ; un devis dont `nftables_admin_ssh` est vide est refusé » (`docs/frontiere-opnsense.md` §5) | fonctionnelle | **P24** (`scripts/devis_opnsense.py --verifier`) | ✅ | Garde anti-lockout : sans cet intrant, la règle SSH n'aurait aucune source et le `block` final fermerait l'accès. |
| AFF-104 | « La frontière déclare toujours une source d'administration ; un devis dont `nftables_admin_ssh` est vide est refusé » (`docs/conception/frontiere-opnsense.md` §5) | fonctionnelle | **P24** (`scripts/devis_opnsense.py --verifier`) | ✅ | Garde anti-lockout : sans cet intrant, la règle SSH n'aurait aucune source et le `block` final fermerait l'accès. |
| AFF-105 | « Les modèles publics valident, underlay compris » (`exemples/modeles/socle/README.md`) | structurelle | **P17** (`scripts/modeles.py verifier`) | ✅ | Underlay facultatif ; validé pour sa cohérence **interne** seulement — un modèle est un gabarit, pas un site déployé. |
| AFF-107 | « Le filtrage est-ouest intra-tenant est dérivé du registre des flux, et redit la même politique que les nftables d'hôte » (`docs/sdn-evpn.md` §3) | structurelle | **P25** (`scripts/devis_proxmox_fw.py --verifier`) | ✅ | 34 groupes, 40 règles sur 2 tenants. Les deux barrières lisent le registre par les **mêmes fonctions** : elles ne peuvent pas diverger. |
| AFF-108 | « Une intégration universelle est déclarée par le rôle, pas recopiée par serveur : aucun hôte n'y échappe et le plan ne la redit pas » (`docs/decisions-architecture.md` D-33) | structurelle | **P26** (`scripts/prouver.py`) | ✅ | L'exemption se dérive du **service rendu** (`sauf_role`), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. |
| AFF-109 | « Ce qui appartient à l'hébergeur (cluster, fabric, frontière) vit dans son dépôt, jamais recopié chez un tenant » (`docs/decisions-architecture.md` D-35) | structurelle | **P27** (`scripts/prouver.py`) | ✅ | Le chemin se dérive du symlink `underlay.yml` : l'hébergeur n'est toujours pas déclaré (D-17). Sautée si aucun underlay n'est monté. |
| AFF-110 | « Chaque tenant a son pool Proxmox, dérivé, sans collision de nom ni de VMID » (`docs/decisions-architecture.md` D-37) | structurelle | **P28** (`scripts/devis_proxmox_pools.py --verifier`) | ✅ | Le nom dérive du dossier d'instance et de l'`index`, déjà unique par P21 — aucun registre de plus. Le devis ne lit pas le cluster : il affirme la cohérence du **plan**, pas l'état appliqué. |
| AFF-111 | « Chaque rôle `serveur_*` déclare sa position d'authentification, et la déclaration est cohérente avec son code » (`docs/authentification.md`, D-38 à D-41) | structurelle | **P29** (`scripts/prouver.py`) | ✅ | 23 rôles déclarés. La preuve refuse l'oubli **et** le mensonge : une déclaration `web-sso natif` sans variable `<rôle>_oidc`, ou `ldap-direct` sans variable `<rôle>_ldap`, échoue. Les indices sont **nommés** — une première version cherchait les mots « ldap »/« oidc » et le mot LDAP dans un commentaire suffisait à valider un mensonge. Les 2 portées `interne-sans-auth` (Prometheus, Loki) sont des lacunes **comptées, pas masquées**. |
| AFF-112 | « Ajouter un tenant implique une zone EVPN, ses VNets et ses sous-réseaux, tous dérivés du seed » (`docs/sdn-evpn.md` §2) | structurelle | **P30** (`scripts/devis_sdn.py --verifier`) | ✅ | 2 zones, 12 VNets, 12 sous-réseaux. La dérivation **reproduit à l'identique** les deux zones déjà présentes sur le cluster (nom, VNI de VRF, MTU, contrôleur) — vérifié par lecture de l'API. La garde refuse un identifiant de VNet de plus de 8 caractères, limite Proxmox qui aurait fait échouer le devis à l'application. Les **nœuds de sortie manquants** sont signalés, pas inventés. |
| AFF-106 | « Le panneau couvre le schéma du plan : aucun champ n'oblige à éditer le YAML » (`docs/intrants-base-gui-conception.md`) | fonctionnelle | **P19** (`scripts/couverture_gui.py`) | 🟡 | Vrai des registres du plan. **Exceptions assumées** : `reseaux` et `hotes` de l'underlay (listes de tables) restent hors du panneau ; les ports physiques et le nœud de sortie EVPN se nomment à la main. |
| AFF-107 | « Le filtrage est-ouest intra-tenant est dérivé du registre des flux, et redit la même politique que les nftables d'hôte » (`docs/conception/sdn-evpn.md` §3) | structurelle | **P25** (`scripts/devis_proxmox_fw.py --verifier`) | ✅ | 34 groupes, 40 règles sur 2 tenants. Les deux barrières lisent le registre par les **mêmes fonctions** : elles ne peuvent pas diverger. |
| AFF-108 | « Une intégration universelle est déclarée par le rôle, pas recopiée par serveur : aucun hôte n'y échappe et le plan ne la redit pas » (`docs/projet/decisions-architecture.md` D-33) | structurelle | **P26** (`scripts/prouver.py`) | ✅ | L'exemption se dérive du **service rendu** (`sauf_role`), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. |
| AFF-109 | « Ce qui appartient à l'hébergeur (cluster, fabric, frontière) vit dans son dépôt, jamais recopié chez un tenant » (`docs/projet/decisions-architecture.md` D-35) | structurelle | **P27** (`scripts/prouver.py`) | ✅ | Le chemin se dérive du symlink `underlay.yml` : l'hébergeur n'est toujours pas déclaré (D-17). Sautée si aucun underlay n'est monté. |
| AFF-110 | « Chaque tenant a son pool Proxmox, dérivé, sans collision de nom ni de VMID » (`docs/projet/decisions-architecture.md` D-37) | structurelle | **P28** (`scripts/devis_proxmox_pools.py --verifier`) | ✅ | Le nom dérive du dossier d'instance et de l'`index`, déjà unique par P21 — aucun registre de plus. Le devis ne lit pas le cluster : il affirme la cohérence du **plan**, pas l'état appliqué. |
| AFF-111 | « Chaque rôle `serveur_*` déclare sa position d'authentification, et la déclaration est cohérente avec son code » (`docs/conception/authentification.md`, D-38 à D-41) | structurelle | **P29** (`scripts/prouver.py`) | ✅ | 23 rôles déclarés. La preuve refuse l'oubli **et** le mensonge : une déclaration `web-sso natif` sans variable `<rôle>_oidc`, ou `ldap-direct` sans variable `<rôle>_ldap`, échoue. Les indices sont **nommés** — une première version cherchait les mots « ldap »/« oidc » et le mot LDAP dans un commentaire suffisait à valider un mensonge. Les 2 portées `interne-sans-auth` (Prometheus, Loki) sont des lacunes **comptées, pas masquées**. |
| AFF-112 | « Ajouter un tenant implique une zone EVPN, ses VNets et ses sous-réseaux, tous dérivés du seed » (`docs/conception/sdn-evpn.md` §2) | structurelle | **P30** (`scripts/devis_sdn.py --verifier`) | ✅ | 2 zones, 12 VNets, 12 sous-réseaux. La dérivation **reproduit à l'identique** les deux zones déjà présentes sur le cluster (nom, VNI de VRF, MTU, contrôleur) — vérifié par lecture de l'API. La garde refuse un identifiant de VNet de plus de 8 caractères, limite Proxmox qui aurait fait échouer le devis à l'application. Les **nœuds de sortie manquants** sont signalés, pas inventés. |
| AFF-106 | « Le panneau couvre le schéma du plan : aucun champ n'oblige à éditer le YAML » (`docs/conception/intrants-base-gui-conception.md`) | fonctionnelle | **P19** (`scripts/couverture_gui.py`) | 🟡 | Vrai des registres du plan. **Exceptions assumées** : `reseaux` et `hotes` de l'underlay (listes de tables) restent hors du panneau ; les ports physiques et le nœud de sortie EVPN se nomment à la main. |
**Non enregistré volontairement.** Les devis eux-mêmes (`make devis-reseau`,
`make devis-opnsense`) ne font l'objet d'aucune affirmation de *justesse* : leur syntaxe
@ -310,16 +316,16 @@ les 🟡 relèvent de la **Phase 3 (arbitrage)**. *(Tous traités depuis — voi
renvoyés au dépôt privé `Set-OPS-modeles`, cohérent avec `exemples/modeles/README.md`.
- **AFF-023 (❌ → résolu, A + correctif d'exemple)** — Chemin de voûte corrigé
`lab/` → `production/` dans `QUICKSTART.md`, `exemples/vault.exemple.yml` et
`docs/config-proxmox.md`. **Piège corrigé** : le modèle `socle` livrait ses intrants en
`docs/exploitation/config-proxmox.md`. **Piège corrigé** : le modèle `socle` livrait ses intrants en
`production/group_vars/all.yml` (forme fichier) ; ajouter `all/vault.yml` (forme dossier)
**masque silencieusement** le fichier `all.yml` (vérifié empiriquement : seul le dossier
est chargé). Le modèle a été converti en forme dossier (`all/10-intrants.yml`), alignée
sur l'instance prouvée `principal`. Validé : `ansible-inventory --host` charge bien
`domaine_interne` depuis la nouvelle disposition.
- **AFF-080/081/082 (❌ → résolu, A)** — Commandes `make` périmées corrigées :
`make help` → `make` (`docs/config-proxmox.md`) ; `make syntax-template` →
`make syntaxe-modele` (`docs/modeles_vm/debian13-proxmox.md`,
`docs/MISE-A-JOUR-CODEX-CLAUDE.md`).
`make help` → `make` (`docs/exploitation/config-proxmox.md`) ; `make syntax-template` →
`make syntaxe-modele` (`docs/exploitation/gabarit-debian13-justification.md`,
`docs/projet/MISE-A-JOUR-CODEX-CLAUDE.md`).
- **AFF-097 (🟡, découvert)** — `lab/` codé en dur restant dans les docs template/clone
(étapes 5+). Consigné ci-dessus ; **non traité dans ce lot** (lot séparé à prévoir).
@ -356,8 +362,8 @@ les 🟡 relèvent de la **Phase 3 (arbitrage)**. *(Tous traités depuis — voi
le token est bien chargé depuis `all/vault.yml` et l'hôte depuis `proxmox.yml`
(assert vert). **Suite, 2026-08-03** : `proxmox.vault.yml` n'est plus chargée du tout — tolérée, elle restait le seul porteur du jeton chez un tenant, dans un fichier gitignoré. Migrée vers `all/vault.yml`, supprimée, et `voute.py verifier` contrôle désormais la voûte RÉELLE quand elle est déchiffrable (ce que la seule vérification du gabarit ne pouvait pas voir).
- **AFF-097 (🟡 → résolu, A)** — `lab/` codé en dur retiré des docs Proxmox/template :
`playbooks/proxmox/README.md`, `docs/procedure-template-debian13-proxmox.md`,
`docs/vm-lifecycle.md`, `docs/modeles_vm/debian13-proxmox.md` → `production/` + voûte
`playbooks/proxmox/README.md`, `docs/exploitation/procedure-template-debian13-proxmox.md`,
`docs/exploitation/vm-lifecycle.md`, `docs/exploitation/gabarit-debian13-justification.md` → `production/` + voûte
unifiée. Plus aucune référence `inventories/lab/group_vars` dans les fichiers suivis
(hors `tmp/` gitignoré et la section *migration* de `config-proxmox.md`, intentionnelle).
@ -394,7 +400,7 @@ commande morte, chemin faux ni étape manquante.*
- **AFF-100 (❌ → résolu, B)** — `instancier.py` et `inventory_gui.py` : le repli sans
`hosts.yml` vise maintenant le **répertoire d'inventaire existant** (→ `production/` pour le
socle) avant de retomber sur `principal`. Instances existantes (avec `hosts.yml`) inchangées.
- **Cohérence GUI/doc** — `docs/intrants-communs.md` : `group_vars/all.yml` →
- **Cohérence GUI/doc** — `docs/conception/intrants-communs.md` : `group_vars/all.yml` →
`group_vars/all/10-intrants.yml` (le GUI écrit déjà en forme dossier). `QUICKSTART.md`
étape 8 : exemple `serveur_postgresql` → `serveur_powerdns` (groupe présent dans le socle).
- **Preuve bout-en-bout (hors-ligne)** : parcours QUICKSTART rejoué sur une copie du socle —

View file

@ -150,7 +150,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 1 | Lis ton réseau physique | : make underlay. Repère les VLAN sous 1000 (l'underlay) et les MTU. Pourquoi le transport doit-il être à 1500 quand l'overlay est à 1450 ? | 👁 observe | — |
| 2 | Regarde sans écrire | : make sdn-plan. Si le cluster dit déjà ce que le plan dit, la sortie tient en une ligne. | 👁 observe | — |
| 3 | Trouve la sortie d'un tenant | : dans make devis-sdn, repère la strophe FRR et l'adresse du prochain saut. À quel équipement appartient-elle ? | 👁 observe | — |
| 4 | Change `index` dans un modèle | (jamais en production) et régénère : combien de valeurs ont bougé ? C'est la mesure exacte de ce que la dérivation t'épargne. Pour aller plus loin : docs/sdn-evpn.md dans le dépôt (référence technique), Le plan & l'adressage dérivé, Mult… | 👁 observe | — |
| 4 | Change `index` dans un modèle | (jamais en production) et régénère : combien de valeurs ont bougé ? C'est la mesure exacte de ce que la dérivation t'épargne. Pour aller plus loin : docs/conception/sdn-evpn.md dans le dépôt (référence technique), Le plan & l'adressage d… | 👁 observe | — |
*Source : [Le réseau des tenants — du câble au VRF](../../wiki/Le-r%C3%A9seau-des-tenants.md) · § À toi de jouer.*

View file

@ -0,0 +1,120 @@
# Preuve de conformite — Set-OPS — 2026-09-30
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
> [`docs/audit/README.md`](README.md), et le registre trace :
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
- **Verdict** : ❌ NON CONFORME (81 OK · 1 echec · 1 saute)
## Preuves
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. |
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 121 flux, schéma + matrice OK. |
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 32 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. |
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 77 scripts expliques et atteignables, 146 cibles make documentees, 69 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 49 document(s) declarent leur lecteur (44 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). |
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 7 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (8 groupe(s) au catalogue). |
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 73 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 202 regle(s) du site. |
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 101 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (139 lignes). |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 309 regles `pass`), tous non consignes et tous motives. |
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (83 preuves, 69 roles, 42 groupes). |
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `3fe7e3c` (publie le 2026-09-29). |
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (7 exposition(s)). |
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 14 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 185 gabarits de role : tous se rendent. |
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ❌ ECHEC | 1 repli(s) silencieux — une derivation vide rend un succes :
- flux : OPS-Chezlepro-lab — perimees par rapport au plan : ops-01.nft |
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 139 etapes, 145 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
## Couverture des affirmations ✅ du registre
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
elles restent hors du harnais recurrent (rien d'executable a rejouer).
## Declarations d'intention (⚪ invérifiables localement — assumees)
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
comme declarations d'intention, non comme preuves :
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
contre une flotte vivante.
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
_Rapport genere le 2026-09-30._

View file

@ -0,0 +1,119 @@
# Preuve de conformite — Set-OPS — 2026-10-01
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
> [`docs/audit/README.md`](README.md), et le registre trace :
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (82 OK · 0 echec · 1 saute)
## Preuves
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. |
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 121 flux, schéma + matrice OK. |
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 32 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. |
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 77 scripts expliques et atteignables, 146 cibles make documentees, 69 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 49 document(s) declarent leur lecteur (45 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). |
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 7 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (8 groupe(s) au catalogue). |
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 73 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 202 regle(s) du site. |
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 101 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (139 lignes). |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 309 regles `pass`), tous non consignes et tous motives. |
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (83 preuves, 69 roles, 42 groupes). |
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `3fe7e3c` (publie le 2026-09-29). |
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (7 exposition(s)). |
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 14 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 185 gabarits de role : tous se rendent. |
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 5 ecosysteme(s) (instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d' |
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 139 etapes, 145 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
## Couverture des affirmations ✅ du registre
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
elles restent hors du harnais recurrent (rien d'executable a rejouer).
## Declarations d'intention (⚪ invérifiables localement — assumees)
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
comme declarations d'intention, non comme preuves :
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
contre une flotte vivante.
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
_Rapport genere le 2026-10-01._

View file

@ -0,0 +1,120 @@
# Preuve de conformite — Set-OPS — 2026-10-03
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
> [`docs/audit/README.md`](README.md), et le registre trace :
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
- **Verdict** : ❌ NON CONFORME (81 OK · 1 echec · 1 saute)
## Preuves
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  |
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 121 flux, schéma + matrice OK. |
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 32 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. |
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 77 scripts expliques et atteignables, 146 cibles make documentees, 69 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 49 document(s) declarent leur lecteur (46 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). |
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 7 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (8 groupe(s) au catalogue). |
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 73 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 202 regle(s) du site. |
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ❌ ECHEC | La carte d'orientation ne dit plus vrai :
- « pieces d'audit » : la carte annonce 50, le depot en compte 51 |
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (139 lignes). |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 309 regles `pass`), tous non consignes et tous motives. |
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (83 preuves, 69 roles, 42 groupes). |
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `3fe7e3c` (publie le 2026-09-29). |
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (7 exposition(s)). |
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 14 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 185 gabarits de role : tous se rendent. |
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 5 ecosysteme(s) (instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d' |
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 139 etapes, 145 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
## Couverture des affirmations ✅ du registre
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
elles restent hors du harnais recurrent (rien d'executable a rejouer).
## Declarations d'intention (⚪ invérifiables localement — assumees)
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
comme declarations d'intention, non comme preuves :
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
contre une flotte vivante.
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
_Rapport genere le 2026-10-03._

View file

@ -0,0 +1,130 @@
# Preuve de conformite — Set-OPS — 2026-10-09
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
> [`docs/audit/README.md`](README.md), et le registre trace :
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (94 OK · 0 echec · 0 saute)
## Preuves
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. |
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 119 flux, schéma + matrice OK. |
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 13 hotes, 33 groupes (inventaire dechiffre et parse). |
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 35 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 4 instance(s) federee(s), aucun index en collision. |
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 78 scripts expliques et atteignables, 151 cibles make documentees, 69 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (148 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 50 document(s) declarent leur lecteur (47 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (11 groupe(s) au catalogue). |
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 74 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 3 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal). |
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 201 regle(s) du site. |
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 102 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (137 lignes). |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 304 regles `pass`), tous non consignes et tous motives. |
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (94 preuves, 69 roles, 42 groupes). |
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `e9215a2` (publie le 2026-10-07). |
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 13 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 27 artefact(s) direct(s) tenus par le depot du site. |
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal ». |
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 189 gabarits de role : tous se rendent. |
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 4 ecosysteme(s) (OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d'amorcage, sorties du |
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 144 etapes, 150 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
| P84 | La fiche du site dit exactement ce que chaque locataire porte | — | ✅ OK | 2 fiche(s) de site : chacune dit exactement ce que son locataire porte (copies, racine, inventaire genere). |
| P85 | La face reseau du locataire dit exactement ce que le site en tire | — | ✅ OK | 2 face(s) reseau : chacune dit exactement ce que le site en tire (decouverte, devis Proxmox, frontiere, DNS public, sauvegarde). |
| P86 | Machine par machine, le locataire et Proxmox admettent les memes entrees | — | ✅ OK | 2 locataire(s) : machine par machine et port par port, nftables et Proxmox admettent les memes sources. |
| P87 | La frontiere designe chaque locataire par les identites qu'il publie | — | ✅ OK | 2 locataire(s) : la frontiere les designe par les identites qu'ils publient (supernet, administration, tunnel, groupes, routes, sortie). |
| P88 | Ce qui entre depuis l'Internet chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : regles WAN, redirections et tunnel disent exactement ce que leur face reseau ouvre a tous. |
| P89 | Ce que l'administration atteint chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : par la gestion, le VPN et le tunnel, l'administration atteint exactement ce que leur face reseau lui ouvre. |
| P90 | Ce qui sort d'un locataire vers l'Internet, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : la frontiere laisse sortir exactement ce que leur face reseau publie. |
| P91 | La fiche du site est deposee a jour, et l'inventaire se genere sans le site | — | ✅ OK | 2 locataire(s) : fiche du site deposee a jour, et inventaire genere sans le site identique a l'octet pres. |
| P92 | Chaque locataire a publie sa face reseau a jour | — | ✅ OK | 2 locataire(s) : face reseau publiee, a jour. |
| P93 | La face publie, machine par machine, les parametres de clonage de l'inventaire | — | ✅ OK | 26 machine(s) : la face publie exactement les parametres de clonage de l'inventaire. |
| P94 | Creer, raser et placer un locataire par sa face visent les memes machines | — | ✅ OK | 2 locataire(s) : creer, raser et placer visent par la face les memes machines que par l'instance montee. |
## Couverture des affirmations ✅ du registre
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
elles restent hors du harnais recurrent (rien d'executable a rejouer).
## Declarations d'intention (⚪ invérifiables localement — assumees)
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
comme declarations d'intention, non comme preuves :
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
contre une flotte vivante.
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
_Rapport genere le 2026-10-09._

View file

@ -0,0 +1,131 @@
# Preuve de conformite — Set-OPS — 2026-10-11
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
> [`docs/audit/README.md`](README.md), et le registre trace :
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (95 OK · 0 echec · 0 saute)
## Preuves
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  |
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 123 flux, schéma + matrice OK. |
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 13 hotes, 33 groupes (inventaire dechiffre et parse). |
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 35 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 4 instance(s) federee(s), aucun index en collision. |
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 81 scripts expliques et atteignables, 154 cibles make documentees, 69 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (150 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 51 document(s) declarent leur lecteur (48 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (11 groupe(s) au catalogue). |
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 77 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 3 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal). |
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 175 regle(s) du site. |
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 103 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (141 lignes). |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 278 regles `pass`), tous non consignes et tous motives. |
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (95 preuves, 69 roles, 42 groupes). |
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `e9215a2` (publie le 2026-10-07). |
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 13 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 27 artefact(s) direct(s) tenus par le depot du site. |
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 11 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal ». |
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 189 gabarits de role : tous se rendent. |
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 4 ecosysteme(s) (OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d'amorcage, sorties du |
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 148 etapes, 153 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
| P84 | La fiche du site dit exactement ce que chaque locataire porte | — | ✅ OK | 2 fiche(s) de site : chacune dit exactement ce que son locataire porte (copies, racine, inventaire genere). |
| P85 | La face reseau du locataire dit exactement ce que le site en tire | — | ✅ OK | 2 face(s) reseau : chacune dit exactement ce que le site en tire (decouverte, devis Proxmox, frontiere, DNS public, sauvegarde). |
| P86 | Machine par machine, le locataire et Proxmox admettent les memes entrees | — | ✅ OK | 2 locataire(s) : machine par machine et port par port, nftables et Proxmox admettent les memes sources. |
| P87 | La frontiere designe chaque locataire par les identites qu'il publie | — | ✅ OK | 2 locataire(s) : la frontiere les designe par les identites qu'ils publient (supernet, administration, tunnel, groupes, routes, sortie). |
| P88 | Ce qui entre depuis l'Internet chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : regles WAN, redirections et tunnel disent exactement ce que leur face reseau ouvre a tous. |
| P89 | Ce que l'administration atteint chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : par la gestion, le VPN et le tunnel, l'administration atteint exactement ce que leur face reseau lui ouvre. |
| P90 | Ce qui sort d'un locataire vers l'Internet, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : la frontiere laisse sortir exactement ce que leur face reseau publie. |
| P91 | La fiche du site est deposee a jour, et l'inventaire se genere sans le site | — | ✅ OK | 2 locataire(s) : fiche du site deposee a jour, et inventaire genere sans le site identique a l'octet pres. |
| P92 | Chaque locataire a publie sa face reseau a jour | — | ✅ OK | 2 locataire(s) : face reseau publiee, a jour. |
| P93 | La face publie, machine par machine, les parametres de clonage de l'inventaire | — | ✅ OK | 26 machine(s) : la face publie exactement les parametres de clonage de l'inventaire. |
| P94 | Creer, raser et placer un locataire par sa face visent les memes machines | — | ✅ OK | 2 locataire(s) : creer, raser et placer visent par la face les memes machines que par l'instance montee. |
| P95 | Chaque locataire publie la cle qui lira son depot du genome, et sa flotte la connait | — | ✅ OK | 2 locataire(s) : depot et cle de lecture publies, cle connue de leur flotte. |
## Couverture des affirmations ✅ du registre
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
elles restent hors du harnais recurrent (rien d'executable a rejouer).
## Declarations d'intention (⚪ invérifiables localement — assumees)
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
comme declarations d'intention, non comme preuves :
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
contre une flotte vivante.
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
_Rapport genere le 2026-10-11._

View file

@ -17,7 +17,7 @@ principe 10).
## Ce que l'épreuve mesure
**Mesuré** : ce que le dépôt public transmet, seul, à un sysadmin compétent qui ne
connaît pas Set-OPS — depuis `QUICKSTART.md`, sur sa propre grappe Proxmox, du dépôt nu
connaît pas Set-OPS — depuis `README.md` et `docs/setup.md`, sur sa propre grappe Proxmox, du dépôt nu
jusqu'à un socle déployé.
**Non mesuré** : les modèles assemblés (dépôt privé `Set-OPS-modeles`), les services
@ -74,7 +74,7 @@ sysadmin.
Rien d'autre. En particulier, l'auteur ne prépare **ni** le jeton API Proxmox, **ni** les
clés SSH, **ni** le mot de passe Vault, **ni** le moindre fichier d'instance : ce sont
les prérequis de `QUICKSTART.md` §0, et savoir les produire fait partie de l'épreuve.
les prérequis de `docs/setup.md`, et savoir les produire fait partie de l'épreuve.
## Périmètre matériel et sécurité

View file

@ -288,6 +288,15 @@
"description": "Le GROUPE Ansible qui sert cette zone (ex. serveur_nginx).",
"x-source-valeurs": "groupes_edge"
},
"resolution_interne": {
"type": "string",
"title": "Resolution interne",
"description": "`service` : le plancher et la zone menent au service lui-meme, l'edge ne sert qu'aux personnes.",
"enum": [
"edge",
"service"
]
},
"secondaires": {
"type": "array",
"title": "Secondaires",

View file

@ -1,5 +1,5 @@
---
# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.
remote: ssh://git@eregion.chezlepro.ca:2222/Alliance-Boreale/Set-OPS-Public.wiki.git
source: 2bbe122
date: 2026-09-29
remote: ssh://git@forge.genese.internal/genome/set-ops-public.wiki.git
source: 56f8d61
date: 2026-10-15

View file

@ -98,7 +98,7 @@ Une directive qu'aucune garde ne vérifie finit par ne plus être vraie. Chaque
| `socle-identite` | **est** la chaîne d'identité, ne peut pas se déléguer à elle-même | keycloak, openldap |
| `ldap-direct` | protocole non-OIDC lié à LDAP | dovecot, postfix |
| `interne-sans-auth` | interface joignable **dans** le tenant sans authentification, non exposée | prometheus, loki |
| `sans-auth-humaine` | aucun point d'authentification humaine | 23 |
| `sans-auth-humaine` | aucun point d'authentification humaine | 24 |
La preuve refuse **l'oubli et le mensonge** : un rôle sans déclaration, une portée
inventée, un `web-sso` sans accès de secours ou sans posture de formulaire, un `web-sso
@ -107,7 +107,7 @@ déclaration que le code contredit.
**Et depuis le 2026-09-07, sa sœur `P58` garde les habilitations** — ce que P29 ne fait
pas : elle tient les *positions* (qui s'authentifie comment), pas les *droits* (qui obtient
quoi). Voir [`autorisation.md`](autorisation.md) §5.
quoi). Voir [`autorisation.md`](../exploitation/autorisation.md) §5.
**Depuis le 2026-09-06, elle refuse aussi que ce tableau mente.** Les chiffres et les listes
de la troisième colonne sont confrontés aux déclarations réelles. Ils avaient cessé d'être

View file

@ -192,7 +192,7 @@ du secret. La §3.1 est donc raffinée, pas contredite.
~~Reste comme valeur réelle (non bloquant) : **factoriser** le bloc de résolution copié-collé
dans les 4 rôles en un include partagé.~~ **Fait.** Le rôle utilitaire
[`roles/resoudre_base/`](../roles/resoudre_base/README.md) porte la résolution, et **cinq**
[`roles/resoudre_base/`](../../roles/resoudre_base/README.md) porte la résolution, et **cinq**
rôles consommateurs l'incluent (`serveur_forgejo`, `serveur_icinga`, `serveur_icingaweb2`,
`serveur_keycloak`, `serveur_nextcloud`). Le DRY a bien été validé **en le déployant**, comme
prévu — l'épreuve de Keycloak. Cf. §9.

View file

@ -0,0 +1,279 @@
# Contextes : un tronc commun, deux classes (SITE et LOCATAIRE)
> **Pour qui :** le **mainteneur** — comment le moteur sait s'il sert un site ou un locataire, et ce que les deux s'apprennent l'un à l'autre.
> **Statut : arrêtée avec l'exploitant le 2026-10-04.** Rien n'est encore construit ; les
> décisions sont au §7, le chemin au §6.
## 1. Le problème, mesuré
Le moteur ne sait pas dans quel contexte il tourne : **chaque script le devine**. Relevé du
2026-10-04 : **33 scripts** font leur propre déduction, à partir de cinq indices différents.
| Indice | Ce qu'on en déduit | Scripts |
|---|---|---|
| lien `instance/` ou `SETOPS_INSTANCE` | « un locataire est monté » | 23 |
| lien `underlay.yml` ou `SETOPS_UNDERLAY` | « un site est monté » ; son plan est à côté | 8 |
| `SETOPS_INVENTAIRE` | quel inventaire de locataire lire | 8 |
| `../*/plan/nomenclature.yml` | « la fédération », les locataires frères | 11 |
| `../SITE-*/underlay.yml` | « les sites » | 2 |
Les indices ne concordent pas toujours, et chaque désaccord a déjà produit un défaut silencieux :
- **2026-08-14** : `frontiere-plan` voulait poser sur la frontière de Technolibre les règles de
Chezlepro. « La fédération » valait « les locataires de ce site », jusqu'au second site.
- **2026-09-16** : la console du runner du site affichait zéro machine, sans erreur. Elle
cherchait un inventaire de locataire là où il n'y en a pas.
- **2026-10-04** : `make ci`, sur le poste, mélangeait le modèle public et les écosystèmes
réels. P74 lisait `SETOPS_UNDERLAY` (le modèle) ; P82 lisait le lien `underlay.yml` et les
dossiers frères (le site réel).
Les rôles Ansible, eux, ne posent pas ce problème : ils sont **déjà** le tronc commun. Un même
`serveur_postgresql` sert au site et chez un locataire.
## 2. Le modèle
```
Ecosysteme (tronc commun)
/ \
Site Locataire
\ /
`-- contrat --' (associations : un site A des locataires,
un locataire A un site)
```
### 2.1 Le tronc commun : `Ecosysteme`
Ce que tout écosystème possède, quel que soit son contexte :
- un **nom** et un **dépôt** (`SITE-Chezlepro`, `OPS-Technolibre`) ;
- une **voûte** et sa clé (`~/.config/setops-vault-<dépôt>`) ;
- un **plan** (`<dépôt>/plan/`) et un **index**, dont dérive son adressage (le site
aussi depuis le 2026-09-20) ;
- des **machines**, déployées par les **mêmes rôles** : socle, durcissement, PKI, journaux,
métriques, supervision, sauvegarde de son propre état ;
- une **filiation** : le moteur et le commit dont il descend ;
- les **preuves communes** : lint, rendu des gabarits, adressage dérivé, etc.
Méthodes abstraites, que chaque classe **surcharge** : `inventaire()`, `machines()`,
`preuves()`, `verbes()`, `console()`.
### 2.2 `Site(Ecosysteme)`
- **Déclaration** : `underlay.yml` (le matériel, les réseaux `site` et `fabric`) et `plan/`
(`10-intrants.yml`, serveurs, applications, domaines, bases).
- **Inventaire** : dynamique (`site_inventaire.py`). Le site ne dérive rien d'un plan de services ;
sa déclaration est sa forme finale.
- **Ce qu'il porte en propre** : le matériel (hyperviseurs, commutateurs, frontière),
la matérialisation des VM (Proxmox), le SDN, le pare-feu Proxmox, la frontière OPNsense,
le DNS public, le dépôt des sauvegardes des locataires, le cache et les artefacts, la forge
du génome.
- **Relation** : `locataires()`, la liste de `underlay.tenants` résolue en objets
`Locataire`. Le site n'en lit que la **face réseau** (§2.4).
### 2.3 `Locataire(Ecosysteme)`
- **Déclaration** : `plan/` (nomenclature, serveurs, applications, bases, domaines).
- **Inventaire** : généré (`instancier.py` → `hosts.yml`). C'est la **méta-classe** de
[`meta-classe.md`](meta-classe.md) : une définition qui engendre toute la flotte.
- **Ce qu'il porte en propre** : la configuration de ses services, la remise au client.
- **Relation** : `site()`, l'hébergeur que nomme `parente.yml`, résolu en objet `Site`. Le
locataire n'en lit que les **intrants exposés** (§2.4).
### 2.4 Ce que le site et le locataire s'apprennent l'un à l'autre
Les deux entités **s'informent mutuellement**. Relevé du 2026-10-04 : qui décide de chaque
information, où elle vit, et comment elle parvient à l'autre.
**Ce que chacun a sous la main.** Le runner du site porte le moteur, son dépôt **et ceux de
ses locataires** (sans leurs voûtes). Le runner d'un locataire ne porte que le moteur et
**son propre** dépôt. Le poste porte tout.
#### Le site informe le locataire, par trois canaux
**Canal 1 : des copies écrites à la main** dans le dépôt du locataire.
| Information | Décidée par | Tenue chez le site dans | Copiée chez le locataire dans | Contrôle |
|---|---|---|---|---|
| son **index** | le site | `underlay.yml` → `tenants` | `plan/nomenclature.yml` (`index`) | `underlay valider` |
| son **adresse publique** | le site | `opnsense.yml` → `opnsense_ips_publiques` | `10-intrants.yml` (`ip_publique`) | — |
| les **10 intrants de service** : résolveur, cache, binaires, forge du génome, cible de sauvegarde, DNS public, plan d'administration, passerelle | le site (dérivés de son plan, par `site_intrants.py`) | son plan | `10-intrants.yml`, et `serveur_ops.yml` pour la forge | `site_intrants.py --verifier`, seulement là où les deux dépôts sont présents (le poste) |
| sa **racine de confiance** | le site | `ac-racine-site.crt` | le même fichier, copié | — |
| *hors contrat* : un dépôt de la forge du site désigné par son adresse | — | — | `serveur_web_dorsal.yml` (Chezlepro) | **aucun** |
**Canal 2 : une lecture directe, au moment de générer l'inventaire.** `instancier.py` ouvre
l'`underlay.yml` et le plan du site pour écrire le `hosts.yml` du locataire. Mesuré sur
Technolibre, inventaire généré avec puis sans le site monté : **quatre variables changent**.
| Variable du locataire | Avec le site monté | Sans le site |
|---|---|---|
| `chrony_serveurs` | `10.0.4.1` (la frontière) | absente |
| `proxmox_pont` | `t23appl` (le VNet SDN) | absente |
| `proxmox_etiquette_vlan` | aucune (le SDN étiquette) | `1236` |
| `serveur_resolveur_zones_deleguees` | `genese.internal` → `10.37.34.11` | absente |
**Canal 3 : le réseau.** Le runner du locataire **tire** son génome de la forge du site ; il est
né de l'**insémination** par le runner du site.
#### Le locataire informe le site : le site lit et recalcule
| Information | Décidée par | Tenue chez le locataire dans | Parvient au site par |
|---|---|---|---|
| ses **zones** et son adressage | dérivés de l'index | `plan/nomenclature.yml` | le site **lit le fichier** |
| les **VM à matérialiser** | le locataire | `plan/serveurs.yml` → `hosts.yml` | le site **lit les fichiers** (placement, clonage, pools, SDN) |
| ses **flux** | ses rôles et son plan | `meta/flux.yml` des rôles (moteur), croisés avec son `hosts.yml` | le site **recalcule** lui-même, avec **sa** version du moteur et la totalité de l'inventaire du locataire → frontière, NAT, pare-feu Proxmox |
| ses **domaines publics** | le locataire | `plan/domaines.yml` (+ `applications.yml`, `serveurs.yml`) | le site **lit les fichiers** → DNS public secondaire |
| sa **clé de sauvegarde** | le locataire | `inventories/*/group_vars/serveur_backup.yml` | le site **lit le fichier** → compte Unix sur le dépôt |
| ses **accès d'administration** | le locataire | `plan/acces.yml`, `nftables_admin_ssh` | le site **lit les fichiers** → pairs WireGuard, règles d'administration |
Le SDN, lui, ne prend aucun flux : il ne filtre pas. Il ne reçoit que l'index, dont il dérive
la zone, les 6 VNets et les 6 sous-réseaux.
#### À l'exécution, entre machines
Ces échanges-là passent par le réseau, pas par les dépôts. Ils sont déjà déclarés en flux :
le locataire **dépose** ses sauvegardes chez le site (SFTP), **tire** ses paquets, ses binaires
et son génome, **entre** par le tunnel d'administration du site ; le site **réplique** les
zones publiques du locataire (AXFR signé TSIG).
#### Ce que le relevé montre
1. **Le site fouille l'intérieur du locataire.** Six fichiers de son plan et de son inventaire,
`group_vars` compris. Rien ne dit ce que le locataire **accepte** de montrer. Renommer un
champ chez le locataire casse le site sans bruit.
2. **Les flux sont calculés deux fois**, par le locataire pour ses `nftables` et par le site pour
la frontière et Proxmox, chacun avec **sa** version du moteur. Ils concordent tant que les deux
runners tiennent le même commit (c'était le cas le 2026-10-04), mais rien ne l'impose.
3. **Le locataire vit de copies** : douze valeurs et un certificat, recopiés à la main, plus une
valeur hors contrat. La garde qui compare ne tourne que sur le poste ; le runner du locataire
ne peut pas savoir que sa copie a vieilli.
4. **L'inventaire d'un locataire dépend du site monté au moment de le générer.** Généré sur le
runner du locataire, qui n'a pas le dépôt du site, il perdrait son serveur de temps, son SDN
et sa délégation DNS. Ça ne s'est jamais vu, parce que l'inventaire est toujours généré sur le
poste puis versionné.
5. **Deux décisions du site** (l'index, l'adresse publique) vivent en double.
#### Proposition : deux fiches, une dans chaque sens
Chacun **publie** ce qu'il donne à l'autre, dans une fiche **générée** par le moteur, jamais
écrite à la main. Chacun ne lit que la fiche que l'autre lui destine. Plus aucune lecture
croisée, plus aucun recalcul.
- **La fiche du site pour un locataire.** Tout ce que le site lui **attribue** (index, adresse
publique) et lui **offre** : les 10 intrants, sa racine de confiance, et ce que l'instancier
allait lire en douce (serveur de temps, délégation DNS, mode SDN et nom des VNets). Une fiche
**par locataire** : aucun ne voit le plan du site ni ses voisins. L'instancier ne lit plus que
cette fiche, et l'inventaire devient **identique où qu'on le génère**.
- **La face réseau du locataire.** Ce qu'il **demande** au site : VM à matérialiser, zones,
domaines publics, clé de sauvegarde publique, accès d'administration, et **ses flux déjà
résolus** (adresses, ports, protocoles), ceux avec l'extérieur pour la frontière et ceux de
chaque VM pour Proxmox. Le locataire génère déjà ses flux résolus (`flux-genere/*.nft`,
`*.connectivite.json`) : la face réseau en est la partie destinée au site. Le site ne
recalcule plus rien : il applique ce que le locataire publie, après l'avoir confronté à sa
propre politique.
- **Chaque fiche porte l'empreinte de sa source**, et une preuve de chaque côté vérifie que la
fiche reçue correspond à ce que l'autre a publié.
**Où en est l'étape 2 (2026-10-04).** La fiche du site (P84), les faits de la face réseau
(P85) et les flux de chaque machine (P86) existent, et disent exactement ce que les lectures
croisées produisent. La première mesure des flux a trouvé une information que le locataire
jetait : les clients nommés d'un port aussi public, que Proxmox doit admettre nommément. Il
les publie désormais (`sources_declarees`). La frontière, en trois temps : les identités
(P87), les entrées publiques (P88), l'administration (P89) et les sorties (P90) sont faites.
**L'étape 2 est terminée** (2026-10-05). Étape 3 : l'instancier lit la fiche déposée par le site, et
l'inventaire d'un locataire se génère sans son site, à l'octet près (P91). Le locataire publie sa face
réseau (P92) ; les comptes de sauvegarde, le DNS public, le pare-feu Proxmox, la frontière et la
découverte des locataires du site la lisent. La matérialisation aussi : la face publie les
paramètres de clonage (P93), sauf la clé du runner du site. Le site l'ajoute lui-même en
clonant le runner du locataire (`clonage_rendu`, 2026-10-11) : elle ne transite plus par la
face, et un locataire peut calculer sa face sans le plan de son site ; `locataire-creer`, `locataire-raser` et `placement-plan TENANT=`
nomment leur locataire au lieu de le monter, et visent les mêmes machines, à l'argument près de
la ligne `ansible-playbook` (P94, `test_appels_locataire.py`) ; `reconstruire-locataire` les
emploie. Reste : la preuve par reconstruction.
Méthodes du contrat : `site.fiche_pour(locataire)`, `locataire.face_reseau()`. Une classe
n'ouvre jamais les fichiers de l'autre ; une preuve vérifiera la règle.
#### Ce que les fiches donnent : la portabilité
Un locataire qui change de site, pour un déménagement, un plan de reprise ou une émancipation,
n'a plus qu'à **recevoir la fiche de son nouveau site**. Son dépôt ne contient plus rien
d'interne à l'ancien : ni copie d'adresse, ni inventaire généré avec l'ancien site monté. En
face, le nouveau site n'a qu'à lire sa **face réseau**. Aujourd'hui, la même bascule demande de
corriger des copies dans plusieurs fichiers, puis de régénérer l'inventaire avec le nouveau site
monté sur le poste.
## 3. Le poste : un sélecteur
Aujourd'hui, le poste monte les deux contextes **en même temps** (`instance/` + `underlay.yml`),
et `ConsolePoste` hérite de `ConsoleLocataire`. Désormais :
- **Le poste choisit un contexte actif** : un site **ou** un locataire. La console ouvre celui-là,
et seulement celui-là.
- **Une opération qui traverse les deux** nomme ses objets au lieu de les deviner.
`reconstruire-locataire` en est l'exemple : le site matérialise, puis le locataire monte.
L'orchestration devient `site.materialiser(locataire)` puis `locataire.monter()`.
- **Les runners ne changent pas** : celui du site n'a qu'un `Site`, celui d'un locataire qu'un
`Locataire`. Leur contexte est désormais **dit**, plus déduit.
## 4. Qui surcharge quoi
| Méthode | Tronc commun | Site | Locataire |
|---|---|---|---|
| `inventaire()` | abstraite | script dynamique (`underlay.yml`) | `hosts.yml` généré du plan |
| `machines()` | abstraite | VM du site + équipements | VM du plan |
| `adressage()` | dérivé de l'index | zones `site` dérivées, liens `fabric` écrits | 6 zones dérivées |
| `sauvegardes()` | son propre état, vérifié par restauration | + héberge les dépôts des locataires | dépose chez son site |
| `supervision()` | sondes déclarées par les rôles | + matériel, fabric, frontière | — |
| `raser()` / `reconstruire()` | — | `site_raser.py` | `raser.py`, `reconstruire_locataire.py` |
| `preuves()` | preuves communes | + preuves de site (P23, P74, P82…) | + preuves de locataire |
| `verbes()` | `verifier`, `publier`… | `site-*`, `frontiere-*`, `proxmox-*` | `appliquer`, `instancier`, `remise-*` |
| `console()` | — | console SITE | console LOCATAIRE |
## 5. Ce qui ne change pas
- Les **rôles Ansible**, déjà communs.
- Les **formats de plan** : pas dans ce chantier.
- La **doctrine** d'`AGENTS.md`.
## 6. Le chemin, chaque pas prouvé avant le suivant
1. **`scripts/contexte.py`** : `Ecosysteme`, `Site`, `Locataire`, `contexte_actif()`,
`Site.charger(nom)`, `Locataire.charger(nom)`. Tests unitaires. Rien ne l'utilise encore.
2. **Les deux fiches**, générées à côté de l'existant sans rien remplacer :
`site.fiche_pour(locataire)` et `locataire.face_reseau()`. Une preuve vérifie que chaque
fiche dit **exactement** ce que les lectures croisées d'aujourd'hui produisent.
3. **Les consommateurs basculent sur les fiches**, un par un : l'instancier sur la fiche du site
(l'inventaire généré doit rester identique, octet pour octet) ; la frontière, Proxmox, le DNS
public et les comptes de sauvegarde sur la face réseau (chaque devis doit rester inchangé).
4. **`prouver.py`** : chaque preuve déclare son contexte (commun, site, locataire) et reçoit son
écosystème du module.
5. **Les autres scripts**, un par un, vérifiés par `make verifier` et par un devis inchangé.
6. **Une preuve « aucune devinette, aucune lecture croisée »** : les indices du §1, et toute
ouverture d'un fichier de l'autre contexte, interdits hors de `contexte.py`.
7. **Les verbes du Makefile** rangés par contexte.
8. **Les consoles** : le sélecteur et deux consoles (chantier suivant).
9. **OPS-Modele** : un locataire modèle, et sans doute un site modèle, vérifiés chacun dans son
contexte.
Après les étapes 3 et 5, une reconstruction prouve que la flotte n'a pas bougé.
## 7. Décisions et questions ouvertes
### Tranché par l'exploitant
- **Deux classes, `Site` et `Locataire`, qui héritent d'un tronc commun** (2026-10-04).
- **Le poste est un sélecteur** : un contexte actif à la fois (2026-10-04).
- **On commence par le moteur**, les consoles viennent ensuite (2026-10-04).
- **Le site dépose sa fiche dans le dépôt du locataire** (2026-10-04), comme il y amorce déjà
son runner. Le runner du locataire n'a besoin d'aucun accès au dépôt du site, et ne voit
ni le plan du site ni ses voisins.
- **Le contexte actif se nomme dans un fichier `contexte` explicite** (2026-10-04), une seule
valeur : `site:SITE-Chezlepro` ou `locataire:OPS-Technolibre`. Un sélecteur qui monte deux
liens à la fois contredirait sa propre règle. Les liens `instance/` et `underlay.yml` restent
le temps de la bascule, lus par le seul `contexte.py`.
- **Un modèle SITE public, `SITE-Modele`**, à côté d'`OPS-Modele` (2026-10-04). Sans site, la
CI ne peut exercer ni les preuves de site (P74, P81, P82) ni la fiche que le site dépose chez
le locataire.
- **Le site a aussi son `parente.yml`** (2026-10-04) : la filiation est dans le tronc commun.

View file

@ -0,0 +1,149 @@
# Dépôts privés sur la forge du site — conception du chantier
> **Pour qui :** le mainteneur qui décide de ce chantier puis le conduit, et quiconque se demande
> pourquoi un runner ne lit que ses propres dépôts.
Préparé le 2026-10-11. Décisions D1 à D3 prises le même jour par Daniel : les trois recommandations.
## 1. Le constat
Sur la forge du site, l'organisation `genome` porte cinq dépôts, tous **publics** :
| Dépôt | Contenu | Qui en a besoin |
|---|---|---|
| `set-ops-public` | le moteur | tous les runners |
| `set-ops-modeles` | les modèles génériques | le runner du site |
| `site-chezlepro` | la carte de la fabric, le plan du site | le runner du site |
| `ops-chezlepro` | le plan, l'inventaire et la face de Chezlepro | son runner, le runner du site |
| `ops-technolibre` | le plan, l'inventaire et la face de Technolibre | son runner, le runner du site |
Mesuré le 2026-10-11 : depuis le runner de Technolibre, `git ls-remote` sans identifiant lit la
tête de `ops-chezlepro` (`b4fbd85`) et de `site-chezlepro` (`51dae24`). Chaque locataire peut donc
lire le plan de l'autre, et la carte du site. **Aucun n'en a besoin** : chaque runner de locataire
ne clone que le moteur et son propre dépôt ; ce que le site lui donne arrive par la fiche déposée
dans son dépôt.
D'où ça vient : le 2026-09-12, à la première reconstruction du site, les dépôts créés privés ont
cassé le clonage anonyme des runners. Ils sont passés publics, avec l'argument que seuls ceux
qui ont franchi la frontière joignent la forge (`scripts/forge_amorcer.py`). Les autres locataires
en font partie.
## 2. La cible
- **Publics** : `set-ops-public` et `set-ops-modeles`. Ils sont génériques, identiques pour tous.
- **Privés** : `site-chezlepro` et chaque `ops-<locataire>`.
- **Le runner du site** lit et pousse tous les dépôts avec sa clé, déjà enregistrée comme clé de
déploiement en écriture par `genome_pousser.yml`. Mesuré le 2026-10-11 : il lit les cinq dépôts
en SSH, avec une vérification stricte de la clé d'hôte. Rien à créer pour lui.
- **Le runner d'un locataire** lit son seul dépôt, avec une clé de déploiement **en lecture
seule** : la clé publique de son runner, déjà déclarée au plan (`ssh_baseline_cles_admin`) et
dont la privée lui est remise à l'armement depuis sa voûte (`vault_runner_cle_ssh`).
- **Aucun secret ne change de main.** Le locataire publie la clé publique de son runner dans sa
face réseau ; le site l'enregistre sur le dépôt de ce locataire, avec ses propres
identifiants d'administration de la forge (`vault_forgejo_admin`, déjà dans sa voûte).
### Éprouvé le 2026-10-11 (étape 1)
Clés de lecture enregistrées par `make forge-cles`, flux posé sur `site-forge-01` et à la
frontière. Depuis chaque runner, `git ls-remote` en SSH, clé d'hôte de la forge épinglée (lue
sur sa machine par l'accès d'administration) :
| Depuis | son dépôt | l'autre locataire | `site-chezlepro` | `set-ops-public` |
|---|---|---|---|---|
| runner de Technolibre | lu (`0172dd8`) | refusé | refusé | refusé |
| runner de Chezlepro | lu (`74d5567`) | refusé | refusé | refusé |
Le refus vient de la forge (« Deploy Key … is not authorized to read »), **alors que les dépôts
sont encore publics** : une clé de déploiement ne vaut que pour le dépôt qui l'a enregistrée.
Conséquence : le moteur, public, continue de se cloner en HTTPS sans identifiant ; seuls les
dépôts privés passent par SSH.
## 3. Le nœud : l'insémination
Le site amorce le runner d'un locataire **avant** que celui-ci ait reçu sa clé stable (elle
arrive à l'armement, geste humain, après). Pendant l'insémination, le rôle `serveur_ops` clone
pourtant le dépôt du locataire : avec un dépôt privé, ce clonage échoue. Même chose au premier
déploiement du runner du site, dont la clé est neuve.
**Remède proposé : le colis.** Quand un runner ne peut pas encore lire un dépôt privé, le
contrôleur qui joue `serveur_ops` (le runner du site pendant l'insémination, le poste
ailleurs) lui apporte ce dépôt en `git bundle`. Le mécanisme existe déjà : `genome_colis.py` porte
le génome du poste au runner du site. Le runner clone le colis, puis pointe `origin` vers l'URL
SSH de la forge. Ses tirages suivants passent par sa clé, une fois armé.
## 4. Les décisions à prendre
**D1 — Comment le runner d'un locataire lit-il son dépôt ?**
- *Clé de déploiement en SSH (recommandé).* Le runner continue de tirer lui-même ; il garde son
autonomie (émancipation, `make` lancé sur le runner, garde `verifier_genome_a_jour`). Coût : un
flux nouveau, du runner de chaque locataire vers la forge du site, en SSH.
- *Colis seulement.* Aucun flux nouveau, aucune clé de déploiement pour les locataires : le
contrôleur apporte toujours le dépôt. Mais le runner ne tire plus son dépôt lui-même, et sa
garde de génome n'a plus de forge à laquelle se comparer.
**D2 — Sur quelle écoute SSH ?**
- *Le `sshd` de la machine, port 22 (recommandé pour commencer).* Le runner du site et le poste
l'utilisent déjà pour pousser ; l'utilisateur `git` y est contraint par la forge. Le flux
n'ouvre que les adresses des runners de locataires, pas leurs supernets.
- *Le serveur SSH intégré de Forgejo, sur un port dédié.* Il n'expose pas le `sshd` de la
machine, mais sa cohabitation avec le `sshd` existant reste à éprouver avant de s'y fier.
**D3 — La preuve.** Ce chantier touche l'insémination : je propose de le prouver par une
reconstruction complète de Technolibre (raser, créer, inséminer, armer, monter), dépôts
privés, avant de reprendre le lot 3b.
## 5. Ce qui change dans le code
État au 2026-10-11 : tout ce tableau est écrit, sauf la bascule elle-même. La clé d'hôte de
la forge est déclarée dans le plan du site (`forge_ssh_cle_hote`), offerte par la fiche
(`serveur_ops_forge_cle_hote`) et copiée par chaque locataire. Le moteur reste cloné en HTTPS.
| Pièce | Changement |
|---|---|
| `serveur_ops_depots` | un drapeau `prive: true` sur les dépôts `instance`, `tenant` et `hebergeur` |
| `roles/serveur_ops` | URL SSH et clé du runner pour les dépôts privés ; colis du contrôleur quand le runner ne peut pas encore les lire ; clé d'hôte de la forge épinglée |
| `contexte.py` (face) | la face publie la clé publique de lecture du runner (`genome.cle_lecture`) |
| `contexte.py` (fiche) | la fiche du site publie la clé d'hôte SSH de sa forge, que le runner du locataire épingle |
| nouvelle cible `forge-cles` | le site enregistre, d'après les faces, la clé de lecture de chaque locataire sur son dépôt, et retire celles qui ne sont plus publiées |
| `forge_amorcer.py` | crée les dépôts privés comme privés |
| flux | `serveur_forge_site` : entrée SSH depuis les runners des locataires hébergés (paire nouvelle, résolue depuis les faces) ; `serveur_ops` : sortie correspondante |
| `genome_etat.yml` | lit la forge par SSH ou authentifié : l'API anonyme répondrait 404 pour un dépôt privé |
| `sonde-genome-servi` | ne compte plus que les dépôts publics (au moins le moteur) : c'est ce qu'un écosystème neuf clone sans rien détenir ; les privés sont suivis par `genome-etat`, `forge-cles` et les runners |
Inchangés : `sonde-genome-a-jour` (son repère ne compare que `set-ops-public`), le poste (il
pousse déjà en SSH), la console (`etat_forge` lit `origin`, qui deviendra l'URL SSH).
## 6. Les preuves à écrire
- **Hors ligne (`make prouver`)** : chaque dépôt `instance`, `tenant` ou `hebergeur` est déclaré
privé ; chaque face publie une clé de lecture, et cette clé est dans `ssh_baseline_cles_admin`.
- **Sur le terrain (`make forge-etancheite`)**, avec témoins :
- sans identifiant, un dépôt privé refuse et un dépôt public répond ;
- le runner d'un locataire lit son dépôt et **ne lit pas** celui de l'autre ;
- les clés de déploiement de chaque dépôt sont exactement : runner du site en écriture, runner
du locataire en lecture.
- **Par reconstruction** : D3.
## 7. L'ordre de bascule
**Fait le 2026-10-11, étapes 1 à 5.** Bascule par `make forge-cles CONFIRMER=true`, mesurée
ETANCHE par `make forge-etancheite`. Preuve par reconstruction de Technolibre (§D3) : le runner
neuf a reçu son dépôt en colis à l'insémination (il n'avait pas encore de clé), l'a tiré en SSH
une fois armé, et la flotte est remontée avec ses témoins d'état intacts. La reconstruction a
d'abord révélé un dossier SSH manquant sur une machine neuve (corrigé, `3fc16c8`).
Chaque étape se vérifie avant la suivante, et la bascule se défait d'un appel par dépôt
(repasser public).
1. **Éprouver** sur la forge du site, sans rien basculer : enregistrer la clé de lecture du
runner de Technolibre sur `ops-technolibre`, ouvrir le flux pour ce seul runner, et mesurer
qu'il lit son dépôt en SSH.
2. **Construire** le code et les preuves (§5, §6) ; `make verifier`.
3. **Déployer sans basculer** : flux et frontière (`make frontiere-appliquer CONFIRMER=true`,
lancé par Daniel), clés de déploiement, remotes SSH sur les trois runners. Les dépôts sont
encore publics : chaque runner doit déjà tirer en SSH.
4. **Basculer** `site-chezlepro`, `ops-chezlepro` et `ops-technolibre` en privé. Mesurer avec
`make forge-etancheite`, puis la veille des trois consoles.
5. **Prouver** par la reconstruction de Technolibre (D3).
6. Reprendre le lot 3b : les demandes vivent alors dans le dépôt de chaque locataire, lisibles
par lui et par le site seulement.

View file

@ -0,0 +1,460 @@
# DNS : interne et public
> **Pour qui :** le **mainteneur** du DNS, et l'**exploitant** d'un site qui publie les noms
> de ses locataires. Le nom du fichier date du temps où il ne couvrait que l'interne.
Le service DNS interne est la premiere capacite de plateforme.
> **Plancher de resolution independant du DNS.** Le socle (`hosts_statiques`, dans
> `serveur_debian`) genere `/etc/hosts` sur **chaque** VM depuis l'inventaire : tout
> l'ecosysteme se resout par nom **meme serveur DNS eteint** (et au bootstrap, avant
> que PowerDNS ne soit la). PowerDNS devient une **commodite** (zone, externe,
> dynamique), plus un point de defaillance.
## Vue d'ensemble
Deux services distincts, qui ne se parlent pas :
- **la résolution interne** — chaque écosystème résout ses propres noms (`*.internal`) et
l'Internet pour ses machines. Rien n'en sort.
- **le DNS public** — les zones publiques des locataires (`chezlepro.ca`, `technolibre.ca`),
écrites chez le locataire, servies à l'Internet par **deux sites**, sous deux noms.
```mermaid
flowchart LR
NET(("Internet"))
subgraph LOC["Les locataires — vivent au site de Chezlepro"]
direction TB
P1["infra-dns-01 · OPS-Chezlepro · 10.17.19.11<br/>pdns@public :5300<br/>primaire caché, signe DNSSEC"]
P2["infra-dns-01 · OPS-Technolibre · 10.23.19.11<br/>pdns@public :5300<br/>primaire caché, signe DNSSEC"]
end
subgraph SC["Site de Chezlepro"]
direction TB
BIF["bifrost-1<br/>WAN 69.70.26.62:53"]
S1["site-dnspub-01 · 10.37.37.21<br/>dnsdist :1053 → PowerDNS :53<br/><b>dns1.chezlepro.ca</b>"]
BIF -- "redirection :1053" --> S1
end
subgraph ST["Site de TechnoLibre"]
direction TB
REL["relais · VPS OVH<br/>51.79.45.193"]
REQ["requin<br/>opt9 RELAIS · opt10 lien"]
S2["site-dnspub-01 · 10.31.37.21<br/>dnsdist :1053 → PowerDNS :53<br/><b>dns1.technolibre.ca</b>"]
REL -- "tunnel WireGuard" --> REQ
REQ -- "redirection :1053" --> S2
end
P1 & P2 -- "NOTIFY · AXFR signé TSIG" --> S1
P1 & P2 -. "NOTIFY · AXFR signé TSIG<br/>par le lien entre les sites" .-> S2
NET -- "53" --> BIF
NET -- "53" --> REL
```
| Pièce | Rôle | Où | Section |
|---|---|---|---|
| plancher `/etc/hosts` | résout la flotte, DNS éteint | chaque VM | [Trois couches](#trois-couches-et-une-seule-est-optionnelle) |
| Unbound | LE résolveur d'un écosystème | `infra-dns-01` (locataire), `site-dns-01` (site) | idem |
| PowerDNS principal | autorité sur la zone `.internal` | boucle locale, derrière Unbound | idem |
| PowerDNS `pdns@public` | primaire caché des zones publiques | `infra-dns-01` du locataire, `:5300` | [Zones publiques](#zones-publiques) |
| PowerDNS + dnsdist | serveur de noms public du site, secondaire | `site-dnspub-01` de chaque site | [Deux sites](#deux-sites-deux-serveurs-de-noms) |
## Trois couches, et une seule est optionnelle
> **Ce paragraphe decrivait le modele d'avant le 2026-08-24** — un Unbound *sur chaque VM*,
> en opt-in. Ce n'est plus le cas : `client_resolveur` **n'installe plus rien**, et son
> integration est **universelle**, pas elective.
```text
1. hosts_statiques le PLANCHER : /etc/hosts genere sur chaque VM depuis l'inventaire
-> l'ecosysteme se resout DNS eteint. Jamais optionnel.
2. serveur_powerdns l'AUTORITATIF de la zone souveraine (un par ecosysteme)
3. serveur_resolveur le RECURSIF : UN seul Unbound pour tout le tenant, qui recurse
depuis la racine et delegue la zone souveraine a PowerDNS
client_resolveur l'integration : ecrit /etc/resolv.conf pour designer ce resolveur
```
**`client_resolveur` n'installe plus de demon** (2026-08-24). Il en posait un par VM — N
demons identiques de ~21 Mo pour quelques centaines de requetes. Il ne fait plus qu'une
chose : **ecrire `/etc/resolv.conf`**. Son integration est marquee `universelle: true`, et
elle n'a **aucune exemption, pas meme l'hote qui porte le resolveur** : il se sert
lui-meme. L'ancien nom (`client_unbound`) mentait des lors qu'il n'installait plus Unbound.
**L'integration suit l'existence du service, elle ne se declare pas.** Aucun
`serveur_resolveur` au plan rend `client_resolveur_actif` faux et le role ne touche a rien :
l'ecosysteme garde la resolution d'amorcage de cloud-init. C'est un choix valide, pas une
panne.
### Le chemin d'une requête interne
```mermaid
flowchart TB
Q["Une machine du locataire<br/>cherche un nom"] --> H{"/etc/hosts<br/>le plancher"}
H -- "machine de la flotte" --> A1["adresse — sans DNS"]
H -- "autre nom" --> U["Unbound · infra-dns-01:53<br/>serveur_resolveur"]
U -- "*.internal et zone inverse<br/>stub-zone" --> PA["PowerDNS principal<br/>127.0.0.1:5300"]
U -- "tout le reste<br/>récursion depuis la racine" --> NET(("Internet"))
Q -. "locataire neuf, sans résolveur à lui" .-> SD["site-dns-01<br/>le résolveur du site<br/>serveur_resolveur_site"]
```
- **Le résolveur est la seule porte.** Quand PowerDNS partage l'hôte d'Unbound, il écoute
sur la boucle locale (`5300`) : personne ne l'interroge directement (port `derive`,
`roles/serveur_powerdns/meta/flux.yml`).
- **Le site prête ses noms aux locataires neufs.** Tant qu'un locataire n'a pas son
résolveur, `client_resolveur` le fait interroger celui du site (`serveur_resolveur_site`,
dont la liste d'autorisation se dérive des locataires hébergés), puis le bascule chez lui.
- **Où mène un nom exposé, vu de l'intérieur** : `resolution_interne` dans
`plan/domaines.yml`. `service` (le site) : la zone mène au service lui-même — l'edge est
pour les personnes. `edge` (défaut, les locataires) : la machine passe par l'edge, parce
que certains services n'existent en HTTPS que derrière lui (Keycloak).
- **Une machine qui naît** interroge `dns_amorcage` (la patte de la frontière) tant que le
résolveur de son site n'existe pas.
## Groupes
```text
serveur_powerdns -> autoritatif interne (PowerDNS Authoritative)
serveur_resolveur -> LE recursif du tenant (Unbound), un seul
client_resolveur -> integration universelle : designe ce resolveur dans /etc/resolv.conf
```
## Zone initiale
La zone initiale est :
```text
exemple.internal
```
Elle est definie dans :
```text
instance/inventories/<inventaire>/group_vars/serveur_powerdns.yml
```
## Enregistrement automatique
Le role `serveur_powerdns` genere la zone a partir de l'inventaire.
Les hotes qui remplissent ces conditions sont ajoutes automatiquement :
```text
hote dans hotes_actifs
ansible_host defini
```
Exemple :
```text
web-frontal-01 ansible_host=10.0.2.31
-> web-frontal-01.exemple.internal A 10.0.2.31
```
Les enregistrements additionnels explicites sont dans `serveur_powerdns_records`.
## Backend PowerDNS
Le premier jalon utilise le backend BIND de PowerDNS.
Raison :
- pas de dependance prematuree a PostgreSQL ;
- zone lisible et generee par Ansible ;
- idempotence simple ;
- bon point de depart pour la supervision.
Quand `serveur_postgresql` sera stable, il sera possible de migrer vers un backend SQL si le besoin operationnel le justifie.
## La bascule de `/etc/resolv.conf` est protegee
Le role `client_resolveur` ne bascule `/etc/resolv.conf` qu'apres avoir verifie que le
resolveur repond **deja** — pour l'interne *et* pour l'Internet :
```yaml
client_resolveur_apply: true
client_resolveur_confirm: true
```
Sans ces deux variables, le role prépare Unbound mais ne modifie pas `/etc/resolv.conf`.
Cette protection est volontaire : une mauvaise configuration DNS peut couper la resolution
de noms. C'est la dependance la plus dangereuse du lot — basculer un hote sur un resolveur
qui n'est pas encore pret le rend **muet**, et le runner qui devrait reparer tombe avec les
autres. Le playbook de groupe applique donc l'hote qui *porte* le resolveur avant ceux qui
s'y adressent, et la preuve **P44** refuse tout ecart entre cette declaration et lui.
## Le piege qui a coute deux jours : la racine signee nie notre TLD
`internal.` n'est **pas delegue dans la racine**, qui est signee : elle rend donc une preuve
NXDOMAIN *validee* pour ce TLD. Or `harden-below-nxdomain` — **actif par defaut** dans
Unbound — tient ce « non » pour prouve et repond NXDOMAIN pour **tout** nom sous
`internal.` depuis son cache, **sans jamais interroger la `stub-zone`** declaree plus bas.
La delegation etait correcte. L'autoritatif repondait juste. Pas une requete ne lui
parvenait.
Ce qui declenche l'empoisonnement : n'importe quelle question sur un nom inexistant sous
`internal.` — y compris la zone d'**un autre ecosysteme**, que ce resolveur ne sert pas et
va donc chercher a la racine. Sur un resolveur partage, ca arrive en permanence.
Et la panne parait **intermittente** : au redemarrage le cache est vide, tout fonctionne, on
conclut que c'est regle. Le remede mesure (2026-09-02) est `harden-below-nxdomain: no` dans
`roles/serveur_resolveur/templates/setops.conf.j2` — **pas** `aggressive-nsec`, qui traite
un autre symptome.
## Surveillance a prevoir
Les premiers checks utiles :
- service `pdns` actif ;
- port UDP/TCP 53 disponible ;
- SOA de `exemple.internal` resoluble ;
- enregistrement `ns1.exemple.internal` resoluble ;
- enregistrements des hotes actifs presents ;
- serial de zone attendu ;
- latence de resolution.
## Zones publiques
> Ajouté le 2026-09-16. Le mode `primaire-cache` était **validé par le schéma et consommé par
> rien** depuis sa création — *une autorité qu'on s'attribue sans l'exercer est une panne
> différée*.
`plan/domaines.yml` porte un champ `autorite` par domaine. Deux valeurs sont exercées :
| `autorite` | Ce que ça veut dire | Qui sert la zone |
|---|---|---|
| `auto-heberge` | zone **interne** (`.internal`), servie à l'écosystème seul | l'instance principale de `serveur_powerdns`, sur la boucle locale |
| `primaire-cache` | zone **publique** : le locataire l'écrit, le site la sert | `pdns@public` chez le locataire (primaire caché), `serveur_dns_public` au site (secondaire public) |
`delegue` est accepté par le validateur et **n'est consommé par aucun rôle**. Il ne faut pas
le déclarer en croyant qu'il fait quelque chose.
**Le locataire écrit, le site sert, un site pair réplique.** Le primaire reste chez le
locataire : quand il part, il emporte sa zone. Le détail — et ce que l'épreuve de PowerDNS a
appris — est dans `roles/serveur_dns_public/README.md`.
Trois règles, chacune payée ou mesurée :
- **TSIG seul ouvre le transfert.** `allow-axfr-ips` et TSIG sont *alternatifs* : une adresse
listée obtient la zone sans signature. L'instance publique n'autorise que la boucle locale.
- **Une instance à part pour le public.** Faire écouter l'instance principale sur l'adresse
de l'hôte aurait permis au serveur public du site d'interroger la zone `.internal`.
- **Le serial suit le contenu.** Un serial figé ferait garder au secondaire l'ancienne zone
pour toujours.
**Rien n'est exposé à Internet en phase 1.** P82 refuse d'exposer le serveur public tant
que toutes ses zones ne sont pas signées DNSSEC.
### Ce qu'une zone publique porte
> Ajouté le 2026-09-16, phase 2. Une zone qui ne portait que ses expositions web aurait coupé
> le courriel de production à la minute où le registraire l'aurait suivie.
Chaque zone `primaire-cache` déclare ses **enregistrements** au plan (`enregistrements:` dans
`plan/domaines.yml`) : A, AAAA, CNAME, MX, TXT, CAA. `valider_domaines` refuse une adresse
privée, une cible incomplète, un CNAME à l'apex ou à côté d'un autre type, un MX sans
priorité, un TXT avec guillemets ou hors ASCII, un nom écrit en absolu (`mx.chezlepro.ca`
sous `chezlepro.ca`), et tout enregistrement dans une zone que Set-OPS n'écrit pas.
Le **SOA et les NS** ne se déclarent pas : ils désignent le serveur de noms du site, sous le
nom que **le site** déclare (`dns_public_nom`, contrat du site, reçu par chaque locataire).
Seule la zone qui contient ce nom porte son A. Le premier choix, `ns1.<zone>`, aurait déplacé
en silence un `ns1.chezlepro.ca` qui existe en production vers une autre adresse.
**Avant de basculer : `make dns-bascule-devis`.** Il compare le plan au DNS en service, nom
par nom et type par type, sur les noms déclarés et une liste de sondes usuelles. Il rend ce
qui serait **perdu**, **changé**, **ajouté**, ou **abandonné en connaissance de cause** (les
marques `heritage=external-dns`), et les préalables : le serveur de noms doit déjà résoudre
publiquement, et il en faut **deux** (exigence du registre `.ca`). Une mesure qui échoue rend
« mesure impossible », jamais « absent ». Sa limite est écrite à chaque rapport : sans
transfert de zone, un export chez le fournisseur actuel reste la seule preuve d'exhaustivité.
Le devis sonde aussi **la colle de chaque serveur désigné** — ceux du site et ceux des pairs
([Deux sites](#deux-sites-deux-serveurs-de-noms)).
Au 2026-10-14 : les deux zones sont servies par les deux sites, signées, au même serial ;
**aucune n'est basculée chez son registraire**. `chezlepro.ca` reproduit la production
(rien ne serait perdu). La décision pour `technolibre.ca` revient au responsable désigné de
TechnoLibre.
### Signature DNSSEC : le locataire signe, avec la clé de sa voûte
> Ajouté le 2026-09-16, phase 2. Éprouvé sur les machines réelles avant d'être codé.
`dnssec: true` sur une zone `primaire-cache` la fait signer **par le primaire du locataire**,
avec **une** clé CSK ECDSA P-256 tenue dans **sa** voûte (`vault_dnssec_<zone>`). Le site ne
détient aucune clé : il reçoit la zone déjà signée et la sert telle quelle (PowerDNS pose
`PRESIGNED` au transfert).
| Geste | Commande | Écrit ? |
|---|---|---|
| Créer la clé d'une zone | `python3 scripts/dnssec.py generer <zone>` | la voûte du locataire (copie de sûreté chiffrée, relue avant et après) |
| Lire le DS à remettre au registraire | `make dnssec-ds` | rien — calculé depuis la voûte, sans la machine |
| Voûte, plan et registre concordent-ils ? | `make dnssec-verifier` | rien |
Pourquoi la clé ne naît pas sur la machine : une reconstruction depuis zéro signerait avec
une **autre** clé, le DS du registraire ne correspondrait plus, et le domaine deviendrait
**BOGUS** pour tout résolveur validant, sans qu'aucune machine soit en panne.
Trois règles, chacune mesurée :
- **Changer la clé ou retirer la signature pendant qu'un DS est publié est refusé** par l'outil
de la machine (`setops-dnssec-zone`) comme par `dnssec.py`. Une mesure du DS qui échoue vaut
refus.
- **Le serial servi est l'heure (`SOA-EDIT EPOCH`).** PowerDNS renouvelle ses signatures chaque
semaine (inception au début de la semaine précédente, expiration deux semaines après le
début de la semaine courante). `INCEPTION-EPOCH`, essayé d'abord, ne dépassait notre serial
qu'au moment où les signatures du site expiraient. Avec l'heure, le site retire la zone à
chaque rafraîchissement ; les signatures servies gardent 7 à 14 jours.
- **L'état DNSSEC fait partie du corps de la zone.** Signer ne change aucun enregistrement :
sans la ligne `; DNSSEC : …`, le serial n'aurait pas bougé et le site aurait gardé la version
non signée.
La sonde `zones-publiques` du site alerte sur une zone signée servie **nue**, avertit sous
6 jours de signature restante, et alerte sous 3.
**Ordre de la mise en service** : basculer les serveurs de noms (zone signée, sans DS : le
domaine reste « non sécurisé », pas cassé), vérifier, **puis** remettre le DS au registraire.
Jamais l'inverse.
## Deux sites, deux serveurs de noms
> Ajouté le 2026-10-14. Le registre des `.ca` exige deux serveurs de noms ; deux serveurs
> sur le même réseau tomberaient ensemble. Le second est au site de TechnoLibre, tenu par une
> autre organisation, sur un autre réseau.
**Chaque site sert les zones publiques de tous les locataires qu'il connaît : les siens, et
ceux de son pair.** Les deux serveurs sont **secondaires** l'un comme l'autre, et tirent
chacun la zone **directement du primaire du locataire** — jamais l'un de l'autre
(`disable-axfr=yes` au site). Un site qui tombe n'emporte donc pas la copie de l'autre.
| Site | Serveur | Nom public | Adresse publique | Chemin depuis l'Internet |
|---|---|---|---|---|
| Chezlepro | `site-dnspub-01` · `10.37.37.21` | `dns1.chezlepro.ca` | `69.70.26.62` | WAN de `bifrost-1` → `:1053` |
| TechnoLibre | `site-dnspub-01` · `10.31.37.21` | `dns1.technolibre.ca` | `51.79.45.193` | relais (VPS) → tunnel `opt9` de `requin` → `:1053` |
### Publier une zone
```mermaid
sequenceDiagram
autonumber
participant X as Exploitant du locataire
participant P as pdns@public<br/>infra-dns-01 du locataire
participant C as site-dnspub-01<br/>site de Chezlepro
participant T as site-dnspub-01<br/>site de TechnoLibre
X->>P: rôle serveur_powerdns — la zone est rendue depuis le plan
Note over P: SOA et NS = dns1 du site + dns1 de chaque pair<br/>serial = suit le contenu · signée avec la clé de la voûte
P-)C: NOTIFY (only-notify : les seuls serveurs du contrat)
P-)T: NOTIFY, par le lien entre les sites
C->>P: SOA ? (UDP 5300)
C->>P: AXFR signé TSIG (TCP 5300)
P-->>C: zone déjà signée — servie telle quelle (PRESIGNED)
T->>P: SOA ? puis AXFR signé TSIG, par le lien
P-->>T: zone déjà signée
```
- **Le locataire écrit, les sites servent.** Les deux sites ne détiennent ni la zone en
source, ni la clé DNSSEC.
- **`only-notify` et `also-notify`** bornent les notifications aux adresses du contrat du
site (`dns_public_site` et `dns_public_pairs`). Sans eux, le primaire notifierait les
adresses publiques de ses NS, depuis l'intérieur du locataire.
- **Le transfert passe par le lien** quand le secondaire est sur l'autre site : la frontière
du site où vit le locataire laisse entrer le transfert et sortir les NOTIFY ; celle du site
qui réplique pose une route `/32` vers chaque primaire. Le devis les dérive de la fiche que
porte chaque locataire (où il vit), et P24 juge ces routes à part.
### Une requête de l'Internet
```mermaid
sequenceDiagram
participant R as Résolveur sur Internet
participant F as Frontière<br/>bifrost-1 ou requin
participant D as dnsdist :1053
participant A as PowerDNS :53
R->>F: chezlepro.ca MX ? (port 53)
Note over F: TechnoLibre : arrive par le relais et le tunnel opt9,<br/>et repart par lui (reply-to RELAIS_GW)
F->>D: redirection vers :1053
Note over D: refuse NOTIFY, UPDATE, AXFR · tronque ANY en UDP<br/>débit borné par /24
D->>A: requête admise
A-->>D: réponse signée
D-->>R: réponse
```
**Sans `reply-to`, la réponse du site de TechnoLibre se perdrait** : elle repartirait par la
route par défaut, le WAN, qui n'a qu'une adresse privée en amont (mesuré le 2026-10-13).
### Les flux
| Flux | De | Vers | Port | Chemin |
|---|---|---|---|---|
| Requête publique | Internet | dnsdist du site | 53 → 1053, UDP et TCP | redirection de la frontière ; par le relais au site de TechnoLibre |
| Requête admise | dnsdist | PowerDNS du site | 53, boucle de l'hôte | local |
| NOTIFY | `pdns@public` du locataire | PowerDNS de chaque site | 53/UDP | local ; par le lien vers l'autre site |
| SOA, puis AXFR TSIG | PowerDNS de chaque site | `pdns@public` du locataire | 5300, UDP puis TCP | local ; par le lien depuis l'autre site |
| Résolution interne | machine du locataire | Unbound de son écosystème | 53 | dans l'écosystème |
| Zone `.internal` | Unbound | PowerDNS principal | 5300, boucle locale | local |
| Récursion | Unbound | Internet | 53 | sortie de la frontière |
Ces flux sont déclarés par les rôles (`meta/flux.yml` de `serveur_dns_public`,
`serveur_powerdns`, `serveur_resolveur`) et par le lien (`opnsense_liens_sites`) ; le devis
de la frontière et le pare-feu Proxmox en dérivent leurs règles. Aucun n'est écrit à la
main sur une frontière.
### Ce qui se déclare, et où
| Clé | Fichier | Ce qu'elle décide |
|---|---|---|
| `autorite: primaire-cache`, `dnssec`, `enregistrements` | `plan/domaines.yml` du **locataire** | la zone est publique, signée, et ce qu'elle porte |
| `dns_public_nom` | `plan/10-intrants.yml` du **site** | le nom de SON serveur de noms (`dns1.<domaine de l'organisation>`) |
| `dns_public_pairs` | plan du **site**, des deux côtés | le site pair qui réplique ; `inactif` n'émet rien |
| `opnsense_liens_sites` | `opnsense.yml` du **site** | le tunnel que prennent NOTIFY et transferts |
| `opnsense_wan_ip` | `opnsense.yml` du **site** | l'adresse publique de son serveur de noms |
Le **contrat du site** (`make site-intrants`) en tire, pour chaque locataire, ce qu'il
dépose dans sa fiche (`fiche-site.yml`) : `dns_public_site`, `dns_public_pairs`,
`dns_public_nom` et `dns_public_noms_pairs` (le nom et l'adresse publique du serveur de
chaque pair, lus dans **son** plan et **sa** carte, par les mêmes fonctions que pour le site
lui-même).
### Les NS d'une zone
```mermaid
flowchart LR
PC["plan du site de Chezlepro<br/>dns_public_nom: dns1.chezlepro.ca"] --> K["contrat du site<br/>fiche du locataire"]
PT["plan du site de TechnoLibre<br/>dns_public_nom: dns1.technolibre.ca<br/>+ opnsense_wan_ip"] -- "dns_public_noms_pairs" --> K
K --> Z["zone-publique.db.j2<br/>chez le locataire"]
Z --> NS["chezlepro.ca — NS dns1.chezlepro.ca, NS dns1.technolibre.ca<br/>A de dns1.chezlepro.ca : seulement dans chezlepro.ca<br/>A de dns1.technolibre.ca : seulement dans technolibre.ca"]
```
- **Un site, un nom** : `dns1` sous le domaine de l'organisation qui tient le site. Pas
`ns1`/`ns2`, qui existent en production à d'autres adresses.
- **Seule la zone qui contient un nom en porte le A**, à l'adresse publique de **son** site.
Les autres zones ne le désignent que par son nom.
- **Sans pair, le rendu est identique octet pour octet** à celui d'un site seul.
### Les gardes
| Garde | Où | Ce qu'elle refuse |
|---|---|---|
| relation des deux côtés | P82 (`prouver._dns_paires`) | un pair déclaré d'un seul côté — le second serveur serait vide sans que personne le voie |
| un pair nomme son serveur | P82 (`prouver._dns_paires`) | un pair actif sans `dns_public_nom` — ses zones n'en désigneraient qu'un, alors qu'il les sert |
| signé avant exposé | P82 | exposer un serveur public dont une zone n'est pas signée |
| nom et adresse du pair | `serveur_powerdns` (`zones-publiques.yml`) | un nom mal formé, une adresse privée, un nom égal au nôtre, un nom redéclaré |
| contrat déposé | `make site-intrants-verifier` | une fiche de locataire qui diffère du contrat (une adresse faussée la rend NON CONFORME) |
| routes du lien | P24 | une route `/32` qu'aucune règle du lien n'utilise |
| zone servie nue | sonde `zones-publiques` | une zone signée servie sans signature ; signatures sous 6 jours (avertit), sous 3 (alerte) |
### Basculer chez un registraire
L'ordre, pour chaque domaine :
1. **`make dns-bascule-devis`** — rien ne serait perdu, et chaque serveur désigné résout déjà
publiquement.
2. **La colle** chez le registraire du domaine qui contient le nom : `dns1.chezlepro.ca`
chez celui de `chezlepro.ca`, `dns1.technolibre.ca` chez celui de `technolibre.ca`.
3. **Les deux NS** chez le registraire de la zone basculée. Zone signée, sans DS : le domaine
reste « non sécurisé », pas cassé.
4. **Vérifier** depuis l'Internet, sur les deux adresses.
5. **Puis seulement le DS** (`make dnssec-ds`). Jamais l'inverse.
**État au 2026-10-14** : les deux sites servent `chezlepro.ca` et `technolibre.ca` au même
serial, chacune avec deux NS signés. **Ni la colle ni les NS ne sont encore chez les
registraires.**

View file

@ -22,7 +22,7 @@ flux:
port: 5432 # entier ou liste [80, 443]
protocole: tcp # tcp | udp
pair: [serveur_keycloak, serveur_forgejo] # QUI — voir « Résolution du pair »
chiffrement: tls-requis # tls-requis | tls | starttls | ssh | tls-cible | clair | n-a (bonus audit)
chiffrement: tls-requis # tls-requis | tls | starttls | ssh | tls-cible | wireguard | clair | n-a (bonus audit)
# tls-requis : chiffré + pair vérifié (verify-full) | tls : chiffré | starttls : mise à niveau opportuniste
# ssh : transport SSH (chiffré, hôte vérifié) | tls-cible : TLS visé mais pas encore appliqué (feuille de route)
# clair : non chiffré (local ou terminé à l'edge) | n-a : sans objet
@ -63,9 +63,34 @@ Un **résolveur** (miroir de `instancier`) agrège, par serveur, les `flux.yml`
(services + intégrations), résout les `pair`, et produit :
- **`nftables`** : ruleset par serveur (allow des flux résolus, `policy drop` par défaut), consommé
par le rôle `nftables_baseline` ;
- **le registre d'audit** : `docs/registre-flux.md` (généré) + une cible `make flux`.
- **le registre d'audit** : `docs/conception/registre-flux.md` (généré) + une cible `make flux`.
## Activation prudente
### Descriptions conservées par les pare-feux
La `raison` non vide du flux et son rôle alimentent les champs natifs : `comment`
pour nftables et Proxmox, `description` pour le filtrage OPNsense (`descr` pour ses
redirections). Un commentaire de fichier `# ...` ne survit pas au chargement nftables.
Les règles communes (administration, connexions établies, refus) portent aussi leur motif.
Les libellés sont sur une ligne. Le commentaire nftables est borné à 127 octets UTF-8 ;
OPNsense garde sa clé `setops:...` complète et borne le texte qui suit pour rester dans
255 octets. Une raison abrégée reste disponible en entier dans le registre des flux.
Le commentaire Proxmox conserve aussi le pair nommé dans le devis.
Une correction de description apparaît dans `make proxmox-fw-plan` et
`make frontiere-plan`. Son application met à jour le champ sur la règle existante,
puis le relit : elle ne recrée pas la règle et ne modifie ni son activation ni sa
politique. Les confirmations habituelles restent requises pour appliquer ces devis.
`make test` vérifie notamment cette convergence et les limites des champs.
Références des champs natifs : [nftables](https://netfilter.org/projects/nftables/manpage.html),
[API Proxmox](https://github.com/proxmox/pve-firewall/blob/master/src/PVE/API2/Firewall/Rules.pm),
[descriptions OPNsense](https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/DescriptionField.php).
### Activer le filtrage
Activer nftables = **action destructive** (peut couper l'accès) → confirmation explicite +
déploiement graduel (garder l'accès SSH/Ansible, tester par nœud).
@ -80,7 +105,7 @@ déploiement graduel (garder l'accès SSH/Ansible, tester par nœud).
1. ✅ figer le schéma (ce doc) + **piloter** sur postgresql / client_metrique / nginx ;
2. ✅ le **résolveur** (`scripts/resoudre_flux.py`) — agrégation → règles + registre ;
3. ✅ **remplir** tous les rôles (transcription du travail zéro-confiance) ;
4. ✅ **générer** + registre d'audit (`docs/registre-flux.md`, gardé par **P49**) ; puis
4. ✅ **générer** + registre d'audit (`docs/conception/registre-flux.md`, gardé par **P49**) ; puis
**activer** nftables nœud par nœud — fait.
Ce qui a suivi la séquence n'y était pas prévu : le même registre alimente désormais aussi
@ -119,7 +144,7 @@ Le registre confondait deux situations :
| Situation | Exemple | `partage` |
|---|---|---|
| le rôle **ouvre** l'écoute | `serveur_dovecot` lie 12345 (SASL réseau) | absent |
| le rôle **ouvre** l'écoute | `serveur_dovecot` lie 24 (LMTP réseau) | absent |
| le rôle **décrit** celle d'un autre | `serveur_backup` emprunte le sshd de `serveur_debian` | `true` |
Sans cette distinction, la seule co-location légitime de la flotte — `tcp/22` sur le
@ -130,4 +155,3 @@ ignorée, ce qui est pire que de ne pas l'avoir.
**Corollaire à retenir** : un port qu'on **subit** (le défaut amont d'un logiciel) doit être
imposé et déclaré comme les autres. Celui d'Alloy ne l'était pas, et c'est la seule raison
pour laquelle la collision a pu durer des semaines.

View file

@ -4,8 +4,8 @@
> Le pare-feu de bordure de l'écosystème. Ce document fixe les décisions d'architecture
> prises le **2026-07-29**, et décrit le devis **dérivé** qui en découle
> (`make devis-opnsense`). Il complète `docs/flux-conception.md` (le modèle des flux) et
> `docs/registre-flux.md` (la matrice d'audit).
> (`make devis-opnsense`). Il complète `docs/conception/flux-conception.md` (le modèle des flux) et
> `docs/conception/registre-flux.md` (la matrice d'audit).
## 1. Le partage des rôles
@ -430,3 +430,12 @@ Reste à trancher sur une interface **physique** : `ip ?`, `access-group ?`, `sp
déclare 53 en UDP et TCP, la récursion depuis la racine que le choix souverain implique.
Le `block out` de la section 5 est donc un vrai default-deny **assumé**, et le devis
l'énonce désormais au lieu de le poser en silence.
- **La sortie générale n'existe plus — 2026-10-10.** Le 123 était déjà passé à la frontière
(2026-09-11). Le 443 et le 80 du socle sont devenus conditionnels (`seulement_si:
artefacts_amorcage` vide) : le site et ses locataires prennent apt par le cache du site,
et ne les reçoivent plus. Mesurée à la frontière, cette sortie ne servait plus qu'à ce
qu'aucun rôle ne déclarait : télémétrie de Grafana, d'Alloy et de Loki et vérification
de version de Collabora (coupées à la source), cartes de rspamd et repère du génome
(déclarés dans `serveur_rspamd` et `serveur_ops_site`). Chaque sortie vers Internet porte
désormais le nom du rôle qui en a besoin. `test_conditions_site.py` refuse qu'un rôle
universel en rouvre une sans condition.

View file

@ -39,7 +39,7 @@
> annonçait « Système / services (SSSD, etc.) → LDAP direct » jusqu'au 2026-09-06. Le dépôt
> ne fait pas ça : le rôle `client_ldap` a été **retiré le 2026-07-04, hors conception**, et
> aucun rôle n'installe SSSD. L'annuaire sert les **applications** ; l'accès à un hôte se
> fait par **clé SSH**, et l'élévation par `sudo`. Voir `docs/authentification.md` §2.
> fait par **clé SSH**, et l'élévation par `sudo`. Voir `docs/conception/authentification.md` §2.
## Sens de provisionnement

View file

@ -5,7 +5,7 @@
But : saisir depuis **un endroit unique** les [intrants communs](intrants-communs.md)
de l'écosystème, en distinguant **constantes** et **défauts surchargeables**.
> Extension assumée du plan de contrôle (gelé — cf. [`positionnement.md`](positionnement.md)),
> Extension assumée du plan de contrôle (gelé — cf. [`positionnement.md`](../projet/positionnement.md)),
> au même titre que le [dimensionnement](dimensionnement-ressources.md). Décidée le
> 2026-06-26.

View file

@ -33,7 +33,7 @@ fois**, depuis un endroit unique, puis les laisser se **dériver** ou se **propa
l'instance, `group_vars/all/vault.yml`. **`proxmox.vault.yml` n'est plus lue** (retirée le
2026-08-03) : tolérée « en compatibilité », elle était restée le *seul* porteur du jeton
chez un tenant — et comme `*.vault.yml` est gitignoré, ce jeton ne voyageait avec aucun
dépôt. Une voûte unique qui ne l'était pas. Cf. `docs/config-proxmox.md`.
dépôt. Une voûte unique qui ne l'était pas. Cf. `docs/exploitation/config-proxmox.md`.
- Golden template : `proxmox_clone_vmid_modele`, `proxmox_clone_source_nom`.
- Placement par défaut : `proxmox_clone_noeud`, `proxmox_clone_stockage`,
`proxmox_clone_pont`, format, complet, timeout, disque, interface, démarrer.
@ -63,7 +63,7 @@ journald (rétention) · core_dumps · systemd_ssh_auto.
### H. Secrets Vault communs 🔒 — **voûte unique** `group_vars/all/vault.yml`
> Tous les secrets de l'instance dans **un seul fichier chiffré par environnement**
> (`inventories/<env>/group_vars/all/vault.yml`), gabarit
> [`exemples/vault.exemple.yml`](../exemples/vault.exemple.yml). Un mot de passe, un
> [`exemples/vault.exemple.yml`](../../exemples/vault.exemple.yml). Un mot de passe, un
> endroit. Édité via `ansible-vault edit` (jamais par le GUI).
**La liste ne s'écrit pas ici.** Elle se *recense*, depuis le plan, les rôles des groupes
actifs et les `group_vars` :

View file

@ -1,58 +1,11 @@
# Carte d'orientation Set-OPS
# Les mécanismes transverses
> **Pour qui :** le **mainteneur** — celui qui va *modifier* le moteur. À lire avant
> d'ajouter quoi que ce soit.
>
> Tu viens plutôt **exploiter** un écosystème déjà déployé ? Wiki →
> **Reprendre l'écosystème**. Tu **apprends** le métier ? Wiki → **Accueil**.
> 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`.
Point d'entrée vers le corpus documentaire, et **catalogue des mécanismes transverses** —
ceux qui vivent dans le code et qu'on *re-découvre* sinon. Créée le 2026-07-03 après un audit
du dépôt, **revue le 2026-07-29**. But : ne plus re-déterrer ce qui existe.
Le dépôt est **déjà bien documenté**. Le manque n'était pas la doc du *modèle*, mais (a) un
index « par où commencer » et (b) une carte des *mécanismes* (dispersés dans le code + les
README de rôles). Cette page comble ces deux trous.
### Le dépôt en chiffres
> Ces valeurs sont **mesurées**, pas recopiées : **P48** les recompte et refuse tout écart.
> Elles étaient toutes fausses le 2026-08-26 — de 6 rôles, de 12 pièces d'audit, de 8
> décisions. Aucune ne faisait travailler personne ; mais une carte dont les faits
> vérifiables sont faux cesse d'être consultée, et c'est alors ses **pointeurs** qu'on perd.
| Ce qu'on compte | Combien | Comment on le mesure |
|---|---|---|
| rôles | 69 | `roles/*/` |
| README de rôles | 69 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
| documents | 45 | `docs/*.md` |
| pièces d'audit | 48 | `docs/audit/*` |
| unités de wiki | 27 | `wiki/*.md` |
| décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
## 1. À lire d'abord (dans l'ordre)
| Sujet | Documents |
|---|---|
| **Autorité / gouvernance** | `AGENTS.md` (source d'autorité), `CLAUDE.md`, `docs/MISE-A-JOUR-CODEX-CLAUDE.md` |
| **Le modèle (plan)** | `docs/architecture-set-ops.md` (survol) → `docs/plan-et-generation.md` (à fond) → `docs/meta-classe.md` (concept) |
| **Services, maturité, dette** | `docs/catalogue-services.md` (**la carte de maturité + la cruft y sont déjà**) |
| **Exploitation / VM** | `docs/vm-lifecycle.md`, `docs/procedure-template-debian13-proxmox.md`, `docs/config-proxmox.md`, `docs/nomenclature-vm.md`, `docs/multi-instances.md` |
| **Conceptions de domaine** | `docs/identite-sso.md`, `docs/courriel-conception.md`, `docs/bindings-conception.md`, `docs/dns-interne.md`, `docs/dimensionnement-ressources.md`, `docs/integrations-vm.md` |
| **Supervision & métriques** | `docs/supervision-conception.md` (les verdicts, vers Icinga) → `docs/metriques-conception.md` (les séries, vers Prometheus et Grafana) — deux questions, deux fichiers `meta/`, un même patron : le rôle déclare, le moteur dérive |
| **Réseau / pare-feu** | `docs/flux-conception.md` (le modèle) → `docs/registre-flux.md` (**généré**, matrice d'audit) → `docs/frontiere-opnsense.md` (la bordure nord/sud) ; underlay : `underlay.yml.example` + `make underlay` |
| **Ordre de déploiement** | `docs/couches-deploiement.yml` (couches) + `docs/dependances-groupes.yml` (graphe) → `playbooks/site.yml` (**généré**, `make site`) |
| **Conformité du déployé** | `docs/devis-services.md` — les **cinq devis de service** (`make identite-plan`, `certificats-plan`, `expositions-plan`, `postgresql-plan`, `courriel-plan`) **et les cinq devis d'infrastructure** (`frontiere-plan`, `proxmox-fw-plan`, `sdn-plan`, `underlay-plan`, `placement-plan`). Répondent à ce que `make prouver` ne demande jamais : *ce qui tourne correspond-il à ce qui est déclaré ?* |
| **Preuve / recette** | `docs/audit/affirmations.md` (registre), `make prouver` → `docs/audit/preuve-<date>.md` — **statique** : lit le dépôt, aucun appel réseau ; la conformité du déployé est l'affaire des devis de service (ligne au-dessus), `docs/audit/plan-de-recette.md` (**généré** du wiki), `docs/audit/protocole-operateur-independant.md` |
| **Décisions d'architecture** | `docs/decisions-architecture.md` — les décisions en vigueur (comptées ci-dessus), pourquoi, où lire le détail, et ce qui les garde ; plus les **décisions renversées** et leur cause |
| **SDN / routage** | `docs/sdn-evpn.md` — décision du 2026-08-02 : le routage inter-zone passe des commutateurs aux hyperviseurs (zones EVPN = VRF). **En service** depuis le 2026-08-03 (`routage_tenants: sdn` dans l'`underlay.yml` du site) : les trunks ne portent plus que deux VLAN d'underlay au lieu de quinze, les passerelles `.1` sont anycast sur chaque hyperviseur. Écart mesuré par `make sdn-plan` |
| **Migration de tenant** | `docs/migration-tenant.md` — recette en **neuf étapes (0 à 8)**, machine à états, gardes ; le receveur se construit **avant** tout gel |
| **Exploitation courante** | `docs/runbooks-exploitation.md`, `docs/intrants-communs.md`, `docs/intrants-base-gui-conception.md`, `docs/theme-forgejo-hors-flotte.md` |
| **Pédagogie (le wiki)** | `wiki/` — les unités (comptées ci-dessus) publiées par `make wiki-publier` ; entrer par `wiki/Home.md` |
| **Vision / positionnement** | `docs/ecosysteme-chezlepro.md`, `docs/positionnement.md`, `docs/pouvoirs-set-ops.md` |
## 2. Les mécanismes transverses (et OÙ ils vivent)
## Les mécanismes transverses, et où ils vivent
Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanisme.**
@ -88,10 +41,10 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis
> `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`.
## 3. Maturité & dette
## Maturité et dette
- **Maturité des rôles, échafaudages, rôles-catégories inertes** : voir
`docs/catalogue-services.md` « État d'implémentation » (source de vérité, tenue à jour).
`docs/projet/catalogue-services.md` « État d'implémentation » (source de vérité, tenue à jour).
- **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`,
@ -110,10 +63,10 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis
`docs/dependances-groupes.yml` + `docs/couches-deploiement.yml`, et sont bel et bien
consommées par l'orchestrateur (`scripts/orchestrer.py` → `playbooks/site.yml`).
## 4. Discipline (pour ne plus re-déterrer)
## Discipline
Avant de concevoir ou d'ajouter un mécanisme :
1. lire **cette carte** + le doc du sujet ;
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

View file

@ -7,7 +7,7 @@ Set-OPS se comporte comme une **méta-classe** : on n'écrit pas onze serveurs,
engendre la flotte entière, cohérente. Le réseau, la taille des VM, les groupes, les
liens de bases (DSN) et l'exposition DNS sont **dérivés** — jamais saisis deux fois.
> Concept acté dans [`positionnement.md`](positionnement.md) : « la définition
> Concept acté dans [`positionnement.md`](../projet/positionnement.md) : « la définition
> instancie toute la flotte ». La dérivation des ressources est décrite dans
> [`dimensionnement-ressources.md`](dimensionnement-ressources.md).

View file

@ -18,7 +18,7 @@ des données différentes, pour des consommateurs différents.
| `supervision.yml` | une **sonde** qui rend un verdict avec un TTL | *est-ce cassé ?* | Icinga |
| `metriques.yml` | un **exportateur** qui expose une série | *depuis quand, et vers où ?* | Prometheus → Grafana |
Ce n'est pas une frontière inventée pour l'occasion. `docs/supervision-conception.md` la
Ce n'est pas une frontière inventée pour l'occasion. `docs/conception/supervision-conception.md` la
pose déjà dans l'autre sens :
> Une **métrique à seuil** — durée de collecte, volume de journaux, taux d'occupation —

View file

@ -129,7 +129,7 @@ sable met `false` et affiche « bac à sable ».
2. Régler l'identité (`10-intrants.yml` : domaine **distinct**), le réseau
(`nomenclature.yml` : **`index` distinct** — tout l'adressage en dérive, cf. plus bas)
et la cible Proxmox (`proxmox.yml`).
3. Créer la voûte unique depuis le gabarit (cf. [`config-proxmox.md`](config-proxmox.md)) :
3. Créer la voûte unique depuis le gabarit (cf. [`config-proxmox.md`](../exploitation/config-proxmox.md)) :
`cp exemples/vault.exemple.yml …/group_vars/all/vault.yml` puis `ansible-vault encrypt`.
**Et poser SA clé** — une voûte, une clé : `~/.config/setops-vault-ops-clientx`, nom
dérivé du dossier en minuscules. `python3 scripts/voutes.py etat` confirme que le moteur
@ -146,7 +146,7 @@ moteur, isolé. C'est la base concrète de la **portabilité des tenants**.
> Garde-fou de positionnement : si le nombre d'instances explose, le besoin d'un
> **registre** (lister/choisir, IPAM, secrets centralisés) relève d'outils établis
> (NetBox, AWX, un coffre type Vault HashiCorp) **à adopter aux seuils**, pas à
> réimplémenter dans le moteur. Cf. [`positionnement.md`](positionnement.md).
> réimplémenter dans le moteur. Cf. [`positionnement.md`](../projet/positionnement.md).
## Adressage fédéré : tout dérive du seed `index`
@ -184,9 +184,9 @@ site. Le décalage de +10, retiré le 2026-08-12, confisquait dix valeurs — et
empêchait de lire l'index directement dans l'adresse. Le VLAN (≤ 4094) autoriserait
jusqu'à 308, le VMID bien plus : c'est donc l'adressage IP qui plafonne. Au-delà d'une poignée d'instances
co-localisées, confier l'allocation à un **IPAM** (NetBox) plutôt qu'au moteur — cf.
[`positionnement.md`](positionnement.md).
[`positionnement.md`](../projet/positionnement.md).
## Voir aussi
- [`meta-classe.md`](meta-classe.md) — le plan d'une instance instancie sa flotte.
- [`config-proxmox.md`](config-proxmox.md) — voûte unique + cible Proxmox par instance.
- [`config-proxmox.md`](../exploitation/config-proxmox.md) — voûte unique + cible Proxmox par instance.
- [`intrants-communs.md`](intrants-communs.md) — intrants de base d'une instance.

View file

@ -61,7 +61,7 @@ propriété des *rôles*, la même pour toutes les instances.
- **Nommage/adressage** : tout part du seed `index` et de la `fonction` de l'hôte.
`supernet = 10.<index>.0.0/16`, `zone = 10.<index>.(15+catégorie)`,
`VLAN = 1000 + index×10 + zone`, `VMID = <VLAN><octet-hôte><rang>` — neuf chiffres,
miroir de l'IP. Voir `docs/nomenclature-vm.md`. *(Les formules à cinq chiffres et le
miroir de l'IP. Voir `docs/conception/nomenclature-vm.md`. *(Les formules à cinq chiffres et le
réseau unique `10.0.x` qui figuraient ici décrivaient le modèle d'avant le
multi-instance.)*
- **DSN** (lien application↔base) : `<type>://<proprietaire>:<secret>@<hôte>:<port>/<base>`.

View file

@ -23,7 +23,7 @@
| `serveur_backup_site` | ingress | 22 | tcp | voisins_site | ssh | Les écosystèmes de ce site déposent leur état ici tant qu'ils n'ont pas leur propre dépôt. SFTP par un utilisateur restreint, jamais un compte d'administration : le site reçoit des octets chiffrés par restic, il ne peut pas les lire. |
| `serveur_cache_site` | ingress | 3142 | tcp | voisins_site | clair | Servir les caches des écosystèmes voisins. Debian n'est ainsi téléchargé qu'une fois pour tout le site, et le cache ne voit que des requêtes AGRÉGÉES — jamais quelle machine installe quoi. |
| `serveur_cache_site` | egress | 80 | tcp | externe | clair | Remplir le cache depuis les dépôts Debian amont. En clair parce que les dépôts apt sont signés : l'intégrité vient de la signature, pas du transport. |
| `serveur_cache_site` | egress | 443 | tcp | externe | tls-requis | Les dépôts tiers qui n'existent qu'en HTTPS (smallstep, Grafana, Icinga). Le cache les relaie pour que la flotte n'ait pas à sortir elle-même. |
| `serveur_cache_site` | egress | 443 | tcp | externe | tls-requis | Les dépôts tiers qui n'existent qu'en HTTPS (smallstep, Grafana, Icinga), et le dépôt de binaires (roues PyPI, collections Galaxy, versions GitHub, Nextcloud, Forgejo). Le site les relaie pour que la flotte n'ait pas à sortir elle-même. |
| `serveur_collabora` | ingress | 9980 | tcp | edge | clair | Éditeur servi au navigateur via l'edge (WebSocket WOPI ; TLS terminé à l'edge). |
| `serveur_collabora` | ingress | 9980 | tcp | localhost | clair | Vérifications WOPI serveur→Collabora depuis Nextcloud co-localisé. |
| `serveur_debian` | ingress | 22 | tcp | flotte, externe | ssh | Plan de gestion : administration et déploiement Ansible par SSH (inter-nœud ; l'accès depuis l'extérieur est filtré à l'OPNsense). |
@ -31,9 +31,9 @@
| `serveur_debian` | ingress | frag-needed | icmp | externe | n-a | ICMP « fragmentation nécessaire » entrant : sans lui, un distant ne peut pas nous demander de réduire nos paquets — les transferts se figent. |
| `serveur_debian` | egress | 53 | udp | frontiere | n-a | Résolution de noms à l'amorçage, avant que le résolveur de l'écosystème n'existe. Sans elle, les machines d'un site neuf ne peuvent pas résoudre leurs dépôts de paquets — et rien ne peut donc s'installer, y compris le résolveur lui-même. |
| `serveur_debian` | egress | 53 | tcp | frontiere | n-a | Réponses longues et bascule TCP, obligatoires en DNS. Déclarer l'UDP sans le TCP donne une résolution qui marche jusqu'à la première réponse tronquée. |
| `serveur_debian` | egress | 80 | tcp | externe | clair | Dépôts apt en clair et redirections HTTP des miroirs (l'intégrité vient de la signature des paquets, pas du transport). |
| `serveur_debian` | egress | 80 | tcp | externe | clair | Dépôts apt en clair et redirections des miroirs, pour un écosystème SANS amont d'artefacts. L'intégrité vient de la signature des paquets, pas du transport. |
| `serveur_debian` | egress | 123 | udp | frontiere | n-a | Synchronisation d'horloge (NTP) contre la frontière, autorité de temps de l'écosystème. Une dérive fait échouer la validation des certificats step-ca et l'authentification SSO. |
| `serveur_debian` | egress | 443 | tcp | externe | tls-requis | Dépôts apt en HTTPS (Debian, Smallstep, Grafana, Icinga, Forgejo, Nextcloud) — sans quoi aucun correctif de sécurité n'entre. |
| `serveur_debian` | egress | 443 | tcp | externe | tls-requis | Dépôts apt en direct, pour un écosystème SANS aucune source d'artefacts. Le site et ses locataires passent par le cache du site et ne la reçoivent pas. |
| `serveur_debian` | egress | frag-needed | icmp | externe | n-a | ICMP « fragmentation nécessaire » sortant : c'est ainsi que nos hôtes signalent l'overlay à 1450 aux correspondants distants. |
| `serveur_dns_public` | ingress | 53 | udp | voisins_site | clair | NOTIFY des primaires des locataires du site : une zone publique a change. Le contenu d'une zone publique n'a rien de secret ; ce qui compte est QUI peut le modifier, et c'est TSIG qui le garde, sur le transfert. |
| `serveur_dns_public` | ingress | 53 | tcp | voisins_site | clair | Repli TCP des notifications des primaires des locataires. |
@ -43,8 +43,8 @@
| `serveur_dns_public` | egress | 5300 | udp | primaires_dns_locataires | clair | SOA demande au primaire avant chaque transfert : c'est lui qui dit si la zone a change. Sans lui, le secondaire garde sa premiere copie pour toujours. |
| `serveur_dovecot` | ingress | 24 | tcp | serveur_postfix | tls-requis | Remise LMTP depuis Postfix (edge-mta -> mailstore), en TLS vérifié (lmtp_tls_security_level=verify). |
| `serveur_dovecot` | ingress | 993 | tcp | externe | tls-requis | Accès courriel des utilisateurs (IMAPS). Frontière publique gérée à l'OPNsense. |
| `serveur_dovecot` | ingress | 12345 | tcp | serveur_postfix | tls | Authentification SASL déléguée : Postfix valide les identifiants de soumission contre Dovecot. |
| `serveur_dovecot` | egress | 636 | tcp | serveur_openldap | tls-requis | userdb/passdb : Dovecot résout et authentifie les comptes sur l'annuaire (LDAPS). |
| `serveur_forge_site` | ingress | 22 | tcp | runners_locataires | ssh | Chaque runner de locataire lit son propre depot du genome, prive, avec sa cle de deploiement en lecture seule. Il ne lit ni le depot d'un autre locataire, ni celui du site. |
| `serveur_forge_site` | ingress | 443 | tcp | voisins_site | tls-requis | Servir le génome aux écosystèmes de ce site : c'est de cette forge qu'ils clonent leur moteur, leurs modèles et la carte de la fabric (D-81). Sans ce flux, un écosystème neuf ne peut pas se reproduire. |
| `serveur_forgejo` | ingress | 3000 | tcp | edge | clair | Interface web + Git HTTP derrière un edge : le nginx termine le TLS et parle en clair à la forge. C'est le cas de tout tenant. |
| `serveur_forgejo` | ingress | derive | tcp | flotte, admin | tls | Sans edge devant elle — la forge du SITE — elle sert son propre TLS sur le port du schéma (443), avec le certificat de la machine. `derive` parce que le port vient de `serveur_forgejo_http_port` : écrire 3000 en dur ici serait faux pour elle, et rien ne le signalerait puisque ce flux ne traverse pas la frontière. |
@ -66,7 +66,7 @@
| `serveur_keycloak` | ingress | 8080 | tcp | edge | clair | Console et endpoints OIDC servis au navigateur et aux applications via l'edge (TLS terminé à l'edge). |
| `serveur_keycloak` | egress | 636 | tcp | serveur_openldap | tls-requis | Fédération de l'annuaire OpenLDAP (LDAPS). |
| `serveur_keycloak` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Persistance Keycloak dans PostgreSQL (verify-full). |
| `serveur_loki` | ingress | 1514 | tcp | fabric | clair | Journaux de la frontiere. Elle n'accueille aucun agent et n'emet que du syslog ; sans ce flux, le seul equipement qui voit passer TOUT le trafic n'ecrit nulle part. |
| `serveur_loki` | ingress | 1514 | tcp | fabric | tls-requis | Journaux de la frontiere, en TLS mutuel. Elle n'accueille aucun agent et n'emet que du syslog ; sans ce flux, le seul equipement qui voit passer TOUT le trafic n'ecrit nulle part. |
| `serveur_loki` | ingress | 3100 | tcp | client_journal | tls | Réception des journaux poussés par Alloy (client_journal) en HTTPS (http_tls_config, cert step-ca). |
| `serveur_loki` | ingress | 3100 | tcp | localhost | clair | Requêtes de Grafana co-localisé (datasource Loki en localhost). |
| `serveur_loki` | ingress | 3100 | tcp | fabric | tls | Journaux des hyperviseurs. La machine qui PORTE les VM ecrivait ses journaux nulle part ailleurs que sur son propre disque — invisibles le jour ou c'est elle qui casse. |
@ -74,8 +74,7 @@
| `serveur_nextcloud` | egress | 443 | tcp | edge | tls-requis | Découverte OIDC auprès de Keycloak (via son FQDN publié à l'edge). |
| `serveur_nextcloud` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base de données Nextcloud (verify-full). |
| `serveur_nextcloud` | egress | 9980 | tcp | serveur_collabora | tls-cible | Vérifications WOPI serveur->Collabora (édition en ligne). TLS interne = feuille de route edge->backends. |
| `serveur_nginx` | ingress | 80 | tcp | externe | clair | HTTP entrant — redirection permanente vers HTTPS. |
| `serveur_nginx` | ingress | 443 | tcp | externe | tls-requis | HTTPS entrant — services exposés (terminaison TLS à l'edge). |
| `serveur_nginx` | ingress | 80 | tcp | admin | clair | HTTP depuis le reseau d'administration — redirection permanente vers HTTPS. |
| `serveur_nginx` | ingress | 443 | tcp | flotte | tls-requis | HTTPS depuis le tenant : les FQDN publiés vivent à l'edge (découverte OIDC, appels inter-services par nom). |
| `serveur_nginx` | ingress | 443 | tcp | admin | tls-requis | HTTPS depuis le reseau d'administration : l'exploitant administre les services par leur interface web, servie par l'edge. |
| `serveur_nginx` | egress | derive | tcp | expositions | tls-cible | Proxy vers les backends exposés (host:port dérivés des expose ; TLS interne = roadmap edge→backends). |
@ -87,22 +86,21 @@
| `serveur_ops` | ingress | 8090 | tcp | edge, admin | clair | Console d'exploitation servie par l'edge (TLS terminé à l'edge), et joignable depuis le plan d'administration là où il n'y a pas d'edge. Le GUI lui-même reste sur la boucle locale : c'est nginx qui authentifie devant. |
| `serveur_ops` | egress | 22 | tcp | flotte | ssh | Piloter la flotte — c'est la raison d'être du poste. |
| `serveur_ops` | egress | 443 | tcp | edge | tls-requis | Cloner et resynchroniser le génome depuis la forge de l'écosystème. |
| `serveur_ops` | egress | 443 | tcp | voisins_site | tls-requis | Cloner le genome depuis la forge du site, quand cet ecosysteme n'heberge pas la sienne. |
| `serveur_ops` | egress | 8006 | tcp | externe | tls-requis | API de l'hyperviseur : créer et cloner les VM d'un écosystème descendant. |
| `serveur_ops_site` | egress | 22 | tcp | fabric | ssh | Shell des hyperviseurs : ce que l'API ne couvre pas — configuration reseau, ponts, deplacement de disques. Un pouvoir distinct de l'API, donc declare a part. |
| `serveur_ops_site` | egress | 22 | tcp | serveur_ops_tenant | ssh | Insemination : amorcer le runner d'un tenant — socle, moteur, plan, plancher de resolution — pour qu'il prenne ensuite le relais sur ses propres machines. Ne transporte aucun secret : la voute est remise par un humain. |
| `serveur_ops_site` | egress | 443 | tcp | fabric | tls-requis | API de la frontiere OPNsense : poser les alias et les regles qui ouvrent les flux du tenant qu'on materialise. Preparer le terrain sans cela laisserait un terrain injoignable. |
| `serveur_ops_site` | egress | 443 | tcp | externe | tls-requis | Repere du genome : `git ls-remote` anonyme sur eregion (forge.alliance-boreale.ca) pour dire si la forge du site a pris du retard. Lecture seule, sans identifiant. |
| `serveur_ops_site` | egress | 8006 | tcp | fabric | tls-requis | API de l'hyperviseur : créer, cloner et détruire les VM de la fabric. Le seul flux par lequel un écosystème peut en matérialiser un autre. |
| `serveur_ops_tenant` | ingress | 22 | tcp | runner_site | ssh | Insémination : le runner du SITE amorce ce runner-ci — socle, moteur, plan, plancher de résolution — jusqu'à ce qu'un humain lui remette sa voûte. Vers cette machine seule, jamais vers le reste de l'écosystème. |
| `serveur_postfix` | ingress | 25 | tcp | externe, client_smtp | starttls | SMTP entrant : courrier externe (MX) et notifications internes (client_smtp). |
| `serveur_postfix` | ingress | 587 | tcp | flotte | starttls | Soumission authentifiée (submission) pour les agents internes qui envoient du courrier. |
| `serveur_postfix` | ingress | 465 | tcp | flotte, externe | tls-requis | Soumission authentifiée en TLS direct (submissions, RFC 8314) : clients de courriel des utilisateurs. |
| `serveur_postfix` | ingress | 587 | tcp | flotte, externe | starttls | Soumission authentifiée (STARTTLS obligatoire) : agents internes et clients de courriel des utilisateurs. |
| `serveur_postfix` | egress | 24 | tcp | serveur_dovecot | tls-requis | Remise finale par LMTP au mailstore (Dovecot), en TLS vérifié. |
| `serveur_postfix` | egress | 25 | tcp | externe | starttls | Relais sortant vers les MX distants (STARTTLS opportuniste). |
| `serveur_postfix` | egress | 636 | tcp | serveur_openldap | tls-requis | Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire (LDAPS). |
| `serveur_postfix` | egress | 636 | tcp | serveur_openldap | tls-requis | Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire, et mots de passe de soumission vérifiés par le Dovecot local (LDAPS). |
| `serveur_postfix` | egress | 11332 | tcp | localhost | clair | Filtre milter rspamd co-localisé (antispam + signature DKIM). |
| `serveur_postfix` | egress | 12345 | tcp | serveur_dovecot | tls | Validation SASL des identifiants de soumission contre Dovecot. |
| `serveur_postgresql` | ingress | 5432 | tcp | serveur_keycloak, serveur_forgejo, serveur_icinga, serveur_nextcloud | tls-requis | Connexions applicatives à PostgreSQL (verify-full ; pg_hba hostssl). |
| `serveur_postgresql` | ingress | 9187 | tcp | serveur_prometheus | clair | Metriques PostgreSQL lues par l'observatoire. Series, pas verdicts : ce qui derive lentement — cache qui decroche, connexions qui montent, bases qui grossissent — n'a que le graphe pour se faire voir. En clair pour l'instant, contrairement a `client_metrique` : dette inscrite, a fermer par `--web.config.file` + `client_pki`. |
| `serveur_postgresql` | ingress | 9187 | tcp | serveur_prometheus | tls | Metriques PostgreSQL lues par l'observatoire, en HTTPS verifie. Series, pas verdicts : ce qui derive lentement — cache qui decroche, connexions qui montent, bases qui grossissent — n'a que le graphe pour se faire voir. |
| `serveur_powerdns` | ingress | 5300 | tcp | dns_public_site | clair | AXFR signe TSIG par le serveur DNS public du site, vers l'instance publique uniquement. |
| `serveur_powerdns` | ingress | 5300 | udp | dns_public_site | clair | Interrogation du SOA par le secondaire du site avant chaque transfert. |
| `serveur_powerdns` | ingress | derive | udp | flotte | clair | Zone souveraine. 53 seul sur son hôte, 5300 sur la loopback derrière le résolveur. |
@ -111,9 +109,11 @@
| `serveur_powerdns` | egress | 53 | tcp | externe | clair | Repli TCP de la résolution sortante du serveur autoritatif (réponses dépassant la taille UDP). |
| `serveur_prometheus` | ingress | 9090 | tcp | localhost | clair | Console Prometheus consommée en local par Grafana co-localisé (pas d'exposition inter-nœud). |
| `serveur_prometheus` | egress | 9100 | tcp | client_metrique | tls | Scrape des node_exporter (HTTPS via cert step-ca) sur chaque nœud instrumenté. |
| `serveur_prometheus` | egress | 9100 | tcp | frontiere | clair | Scrape de l'exportateur de la frontiere (os-node_exporter), sur sa seule patte de supervision : l'equipement qui voit passer TOUT le trafic n'avait ni temperature ni charge dans la supervision. |
| `serveur_prometheus` | egress | 9100 | tcp | frontiere | tls | Scrape de l'exportateur de la frontiere (os-node_exporter), sur sa seule patte de supervision : l'equipement qui voit passer TOUT le trafic n'avait ni temperature ni charge dans la supervision. |
| `serveur_prometheus` | egress | 9100 | tcp | fabric | clair | Scrape des hyperviseurs : la machine qui PORTE les VM etait invisible de la supervision qui les surveille. Elle ne peut pas pousser (route par defaut gelee, D-57), donc on la tire. |
| `serveur_redis` | ingress | 6379 | tcp | localhost | clair | Cache/verrous consommés uniquement par l'application co-localisée (ex. Nextcloud). Aucune exposition inter-nœud. |
| `serveur_relais` | ingress | 22 | tcp | externe | ssh | La console d'administration, sur l'adresse PROPRE du VPS seulement. Les adresses relayees ne menent jamais a ce SSH : elles partent dans le tunnel. |
| `serveur_relais` | ingress | derive | udp | externe | wireguard | La frontiere du site ouvre le tunnel depuis son adresse DHCP (port derive : 51800 + index du site). Seule sa cle est admise ; le reste de l'Internet ne recoit aucune reponse. |
| `serveur_resolveur` | ingress | 53 | udp | flotte | clair | Toute la flotte du tenant résout ici — et nulle part ailleurs. |
| `serveur_resolveur` | ingress | 53 | tcp | flotte | clair | Réponses longues et bascule TCP, obligatoires en DNS. |
| `serveur_resolveur` | egress | 53 | udp | externe | clair | Récursion depuis les serveurs racine. Aucun transitaire : l'écosystème ne confie ses questions à personne. |
@ -121,18 +121,22 @@
| `serveur_resolveur_site` | ingress | 53 | tcp | voisins_site | clair | Réponses longues et bascule TCP, obligatoires en DNS. |
| `serveur_rspamd` | ingress | 11332 | tcp | localhost | clair | Protocole milter consommé par Postfix co-localisé (analyse + signature DKIM). Local uniquement. |
| `serveur_rspamd` | ingress | 11334 | tcp | localhost | clair | Interface de contrôle rspamd (statistiques, apprentissage) en local. |
| `serveur_rspamd` | egress | 80 | tcp | externe | clair | Carte de hachages SURBL (sa-update.surbl.org), publiee en clair par l'amont. Une carte alteree ne peut que mal classer un courriel ; elle n'execute rien. |
| `serveur_rspamd` | egress | 443 | tcp | externe | tls-requis | Cartes de rspamd (maps.rspamd.com) : messageries gratuites et jetables, redirecteurs, listes blanches DMARC/SPF/DKIM, hameconnage. Sans elles l'antispam garde les copies du paquet, qui vieillissent. |
| `serveur_step_ca` | ingress | 8443 | tcp | flotte | tls-requis | ACME + API step-ca : chaque nœud (client_pki) émet/renouvelle ses certificats et récupère la racine. |
| `serveur_web_dorsal` | ingress | 80 | tcp | edge | clair | Front nginx local des webapps, proxie par l'edge (TLS termine a l'edge). |
| `serveur_web_dorsal` | ingress | 80 | tcp | edge, serveur_web_frontal | clair | Front nginx local des webapps et des sites statiques, relaye par l'edge (noms internes) et par le web frontal (noms publics). |
| `serveur_web_dorsal` | egress | 443 | tcp | flotte | tls | git clone/pull du depot de chaque app (Forgejo souverain) au deploiement. |
| `serveur_web_frontal` | ingress | 80 | tcp | edge | clair | Contenu statique servi au navigateur via l'edge (TLS termine a l'edge). |
| `serveur_web_frontal` | egress | 443 | tcp | flotte | tls | git clone/pull du depot du site (Forgejo souverain) au deploiement. |
| `serveur_web_frontal` | ingress | 80 | tcp | externe | clair | HTTP public, relaye a travers le WAF — et le defi HTTP d'ACME ; renverra vers HTTPS quand le certificat sera la. |
| `serveur_web_frontal` | ingress | 443 | tcp | externe | tls-requis | HTTPS public — les sites et services que le locataire publie sur l'Internet (attend son certificat Let's Encrypt). |
| `serveur_web_frontal` | egress | 80 | tcp | serveur_web_dorsal | clair | Relais vers le web dorsal, qui porte les sites et les applications publiques. |
## Synthèse chiffrement
- **clair** : 50 flux
- **clair** : 49 flux
- **n-a** : 8 flux
- **ssh** : 8 flux
- **ssh** : 10 flux
- **starttls** : 6 flux
- **tls** : 11 flux
- **tls** : 10 flux
- **tls-cible** : 2 flux
- **tls-requis** : 35 flux
- **tls-requis** : 37 flux
- **wireguard** : 1 flux

View file

@ -11,7 +11,7 @@ que personne ne devine, et elle vieillira sans que quiconque s'en aperçoive.
La série des flux, elle, obéit à une règle propre : ces pages expliquent **comment les
choses sont configurées et interconnectées**. Ce ne sont pas des comptes rendus — elles ne
relatent aucun incident, ne datent aucune panne et ne comptent aucune machine tombée. Ce
travail-là appartient au `CHANGELOG.md` et à `docs/decisions-architecture.md`. Une page de
travail-là appartient au `CHANGELOG.md` et à `docs/projet/decisions-architecture.md`. Une page de
cette série répond à une seule question : *par où passe telle chose, et pourquoi par là*.
## La série

View file

@ -3,7 +3,7 @@
> **Pour qui :** le **mainteneur** du réseau overlay.
> **Décision d'architecture du 2026-08-02.** Elle remplace le routage inter-zone sur les
> commutateurs L3 par des zones EVPN de Proxmox SDN. Complète `docs/frontiere-opnsense.md`
> commutateurs L3 par des zones EVPN de Proxmox SDN. Complète `docs/conception/frontiere-opnsense.md`
> (la bordure, inchangée) et `underlay.yml.example` (la fabric, très allégée).
>
> **Décidé le 2026-08-03** : une zone EVPN **par tenant** ; le routage **et le filtrage**
@ -220,4 +220,4 @@ l'interface étiquetée dédiée retenue.
`voisins_site` (2026-08-24), né du besoin de chaîner les caches d'artefacts. Cf. §3.
- **La migration depuis l'existant** : le tenant Chezlepro tourne déjà sur des VLAN. Passer à
EVPN est un changement de plan de transport pour des VM en service — la recette de
`docs/migration-tenant.md` s'applique-t-elle, ou faut-il un chemin plus court ?
`docs/hebergement/migration-tenant.md` s'applique-t-elle, ou faut-il un chemin plus court ?

View file

@ -23,6 +23,19 @@ couches:
- serveur_debian
- serveur_durci
# LE DEPOT DE L'ETAT AVANT TOUT CE QUI EN RESTAURE (2026-10-13). L'AC, les bases, la
# supervision demandent au depot « mon incarnation precedente attend-elle ? » au moment de
# naitre — et refusent, a juste titre, un depot ILLISIBLE (conclure « rien a restaurer »
# faute de le joindre ferait naitre neuf ce qui attend). Range dans `services`, le depot
# n'existait pas encore : le premier site construit depuis zero (TechnoLibre) s'est arrete
# sur sa PKI. Il ne depend que du socle.
- nom: depot_etat
raison: "Le depot de sauvegarde : il doit repondre (vide ou non) avant que l'AC, les bases et la supervision ne naissent."
groupes:
- serveur_backup
# Le depot du site, meme nature : il recoit l'etat des locataires et le sien.
- serveur_backup_site
- nom: pki_racine
raison: "L'autorité de certification interne (step_ca) : la racine de confiance de tous les flux TLS est-ouest."
groupes:
@ -75,7 +88,6 @@ couches:
- serveur_rspamd
- serveur_dovecot
- serveur_postfix
- serveur_backup
# La source d'artefacts vient AVANT ceux qui installent des paquets — c'est tout
# son objet. Placee plus tard, elle serait remplie apres avoir servi.
- serveur_artefacts
@ -90,8 +102,10 @@ couches:
- serveur_forge_site
# Le resolveur du site, meme nature : prete aux locataires pendant leur jeunesse.
- serveur_resolveur_site
# Le depot de sauvegarde du site, meme nature : il recoit l'etat des locataires.
- serveur_backup_site
# LE RELAIS (2026-10-13) : la face publique d'un site qui n'a qu'une adresse DHCP. Un
# equipement HORS du site (VPS), inventorie depuis la carte comme un hyperviseur. Il
# vient avant tout ce qui se publie : sans lui, une adresse publique ne mene nulle part.
- serveur_relais
# Le DNS public du site vient APRES les services : il tire les zones que les
# autoritatifs des locataires ecrivent, et n'a rien a servir avant eux.
- serveur_dns_public

View file

@ -1,241 +0,0 @@
# DNS interne
> **Pour qui :** le **mainteneur** du DNS interne.
Le service DNS interne est la premiere capacite de plateforme.
> **Plancher de resolution independant du DNS.** Le socle (`hosts_statiques`, dans
> `serveur_debian`) genere `/etc/hosts` sur **chaque** VM depuis l'inventaire : tout
> l'ecosysteme se resout par nom **meme serveur DNS eteint** (et au bootstrap, avant
> que PowerDNS ne soit la). PowerDNS devient une **commodite** (zone, externe,
> dynamique), plus un point de defaillance.
## Trois couches, et une seule est optionnelle
> **Ce paragraphe decrivait le modele d'avant le 2026-08-24** — un Unbound *sur chaque VM*,
> en opt-in. Ce n'est plus le cas : `client_resolveur` **n'installe plus rien**, et son
> integration est **universelle**, pas elective.
```text
1. hosts_statiques le PLANCHER : /etc/hosts genere sur chaque VM depuis l'inventaire
-> l'ecosysteme se resout DNS eteint. Jamais optionnel.
2. serveur_powerdns l'AUTORITATIF de la zone souveraine (un par ecosysteme)
3. serveur_resolveur le RECURSIF : UN seul Unbound pour tout le tenant, qui recurse
depuis la racine et delegue la zone souveraine a PowerDNS
client_resolveur l'integration : ecrit /etc/resolv.conf pour designer ce resolveur
```
**`client_resolveur` n'installe plus de demon** (2026-08-24). Il en posait un par VM — N
demons identiques de ~21 Mo pour quelques centaines de requetes. Il ne fait plus qu'une
chose : **ecrire `/etc/resolv.conf`**. Son integration est marquee `universelle: true`, et
elle n'a **aucune exemption, pas meme l'hote qui porte le resolveur** : il se sert
lui-meme. L'ancien nom (`client_unbound`) mentait des lors qu'il n'installait plus Unbound.
**L'integration suit l'existence du service, elle ne se declare pas.** Aucun
`serveur_resolveur` au plan rend `client_resolveur_actif` faux et le role ne touche a rien :
l'ecosysteme garde la resolution d'amorcage de cloud-init. C'est un choix valide, pas une
panne.
## Groupes
```text
serveur_powerdns -> autoritatif interne (PowerDNS Authoritative)
serveur_resolveur -> LE recursif du tenant (Unbound), un seul
client_resolveur -> integration universelle : designe ce resolveur dans /etc/resolv.conf
```
## Zone initiale
La zone initiale est :
```text
exemple.internal
```
Elle est definie dans :
```text
instance/inventories/<inventaire>/group_vars/serveur_powerdns.yml
```
## Enregistrement automatique
Le role `serveur_powerdns` genere la zone a partir de l'inventaire.
Les hotes qui remplissent ces conditions sont ajoutes automatiquement :
```text
hote dans hotes_actifs
ansible_host defini
```
Exemple :
```text
web-frontal-01 ansible_host=10.0.2.31
-> web-frontal-01.exemple.internal A 10.0.2.31
```
Les enregistrements additionnels explicites sont dans `serveur_powerdns_records`.
## Backend PowerDNS
Le premier jalon utilise le backend BIND de PowerDNS.
Raison :
- pas de dependance prematuree a PostgreSQL ;
- zone lisible et generee par Ansible ;
- idempotence simple ;
- bon point de depart pour la supervision.
Quand `serveur_postgresql` sera stable, il sera possible de migrer vers un backend SQL si le besoin operationnel le justifie.
## La bascule de `/etc/resolv.conf` est protegee
Le role `client_resolveur` ne bascule `/etc/resolv.conf` qu'apres avoir verifie que le
resolveur repond **deja** — pour l'interne *et* pour l'Internet :
```yaml
client_resolveur_apply: true
client_resolveur_confirm: true
```
Sans ces deux variables, le role prépare Unbound mais ne modifie pas `/etc/resolv.conf`.
Cette protection est volontaire : une mauvaise configuration DNS peut couper la resolution
de noms. C'est la dependance la plus dangereuse du lot — basculer un hote sur un resolveur
qui n'est pas encore pret le rend **muet**, et le runner qui devrait reparer tombe avec les
autres. Le playbook de groupe applique donc l'hote qui *porte* le resolveur avant ceux qui
s'y adressent, et la preuve **P44** refuse tout ecart entre cette declaration et lui.
## Le piege qui a coute deux jours : la racine signee nie notre TLD
`internal.` n'est **pas delegue dans la racine**, qui est signee : elle rend donc une preuve
NXDOMAIN *validee* pour ce TLD. Or `harden-below-nxdomain` — **actif par defaut** dans
Unbound — tient ce « non » pour prouve et repond NXDOMAIN pour **tout** nom sous
`internal.` depuis son cache, **sans jamais interroger la `stub-zone`** declaree plus bas.
La delegation etait correcte. L'autoritatif repondait juste. Pas une requete ne lui
parvenait.
Ce qui declenche l'empoisonnement : n'importe quelle question sur un nom inexistant sous
`internal.` — y compris la zone d'**un autre ecosysteme**, que ce resolveur ne sert pas et
va donc chercher a la racine. Sur un resolveur partage, ca arrive en permanence.
Et la panne parait **intermittente** : au redemarrage le cache est vide, tout fonctionne, on
conclut que c'est regle. Le remede mesure (2026-09-02) est `harden-below-nxdomain: no` dans
`roles/serveur_resolveur/templates/setops.conf.j2` — **pas** `aggressive-nsec`, qui traite
un autre symptome.
## Surveillance a prevoir
Les premiers checks utiles :
- service `pdns` actif ;
- port UDP/TCP 53 disponible ;
- SOA de `exemple.internal` resoluble ;
- enregistrement `ns1.exemple.internal` resoluble ;
- enregistrements des hotes actifs presents ;
- serial de zone attendu ;
- latence de resolution.
## Zones publiques
> Ajouté le 2026-09-16. Le mode `primaire-cache` était **validé par le schéma et consommé par
> rien** depuis sa création — *une autorité qu'on s'attribue sans l'exercer est une panne
> différée*.
`plan/domaines.yml` porte un champ `autorite` par domaine. Deux valeurs sont exercées :
| `autorite` | Ce que ça veut dire | Qui sert la zone |
|---|---|---|
| `auto-heberge` | zone **interne** (`.internal`), servie à l'écosystème seul | l'instance principale de `serveur_powerdns`, sur la boucle locale |
| `primaire-cache` | zone **publique** : le locataire l'écrit, le site la sert | `pdns@public` chez le locataire (primaire caché), `serveur_dns_public` au site (secondaire public) |
`delegue` est accepté par le validateur et **n'est consommé par aucun rôle**. Il ne faut pas
le déclarer en croyant qu'il fait quelque chose.
**Le locataire écrit, le site sert, un site pair réplique.** Le primaire reste chez le
locataire : quand il part, il emporte sa zone. Le détail — et ce que l'épreuve de PowerDNS a
appris — est dans `roles/serveur_dns_public/README.md`.
Trois règles, chacune payée ou mesurée :
- **TSIG seul ouvre le transfert.** `allow-axfr-ips` et TSIG sont *alternatifs* : une adresse
listée obtient la zone sans signature. L'instance publique n'autorise que la boucle locale.
- **Une instance à part pour le public.** Faire écouter l'instance principale sur l'adresse
de l'hôte aurait permis au serveur public du site d'interroger la zone `.internal`.
- **Le serial suit le contenu.** Un serial figé ferait garder au secondaire l'ancienne zone
pour toujours.
**Rien n'est exposé à Internet en phase 1.** P82 refuse d'exposer le serveur public tant
que toutes ses zones ne sont pas signées DNSSEC.
### Ce qu'une zone publique porte
> Ajouté le 2026-09-16, phase 2. Une zone qui ne portait que ses expositions web aurait coupé
> le courriel de production à la minute où le registraire l'aurait suivie.
Chaque zone `primaire-cache` déclare ses **enregistrements** au plan (`enregistrements:` dans
`plan/domaines.yml`) : A, AAAA, CNAME, MX, TXT, CAA. `valider_domaines` refuse une adresse
privée, une cible incomplète, un CNAME à l'apex ou à côté d'un autre type, un MX sans
priorité, un TXT avec guillemets ou hors ASCII, un nom écrit en absolu (`mx.chezlepro.ca`
sous `chezlepro.ca`), et tout enregistrement dans une zone que Set-OPS n'écrit pas.
Le **SOA et les NS** ne se déclarent pas : ils désignent le serveur de noms du site, sous le
nom que **le site** déclare (`dns_public_nom`, contrat du site, reçu par chaque locataire).
Seule la zone qui contient ce nom porte son A. Le premier choix, `ns1.<zone>`, aurait déplacé
en silence un `ns1.chezlepro.ca` qui existe en production vers une autre adresse.
**Avant de basculer : `make dns-bascule-devis`.** Il compare le plan au DNS en service, nom
par nom et type par type, sur les noms déclarés et une liste de sondes usuelles. Il rend ce
qui serait **perdu**, **changé**, **ajouté**, ou **abandonné en connaissance de cause** (les
marques `heritage=external-dns`), et les préalables : le serveur de noms doit déjà résoudre
publiquement, et il en faut **deux** (exigence du registre `.ca`). Une mesure qui échoue rend
« mesure impossible », jamais « absent ». Sa limite est écrite à chaque rapport : sans
transfert de zone, un export chez le fournisseur actuel reste la seule preuve d'exhaustivité.
Au 2026-09-16 : `chezlepro.ca` reproduit la production (rien ne serait perdu) ;
`technolibre.ca` est **préparée, pas basculée** — la décision revient au responsable désigné
de TechnoLibre, et elle attend que `dns1.chezlepro.ca` soit publié.
### Signature DNSSEC : le locataire signe, avec la clé de sa voûte
> Ajouté le 2026-09-16, phase 2. Éprouvé sur les machines réelles avant d'être codé.
`dnssec: true` sur une zone `primaire-cache` la fait signer **par le primaire du locataire**,
avec **une** clé CSK ECDSA P-256 tenue dans **sa** voûte (`vault_dnssec_<zone>`). Le site ne
détient aucune clé : il reçoit la zone déjà signée et la sert telle quelle (PowerDNS pose
`PRESIGNED` au transfert).
| Geste | Commande | Écrit ? |
|---|---|---|
| Créer la clé d'une zone | `python3 scripts/dnssec.py generer <zone>` | la voûte du locataire (copie de sûreté chiffrée, relue avant et après) |
| Lire le DS à remettre au registraire | `make dnssec-ds` | rien — calculé depuis la voûte, sans la machine |
| Voûte, plan et registre concordent-ils ? | `make dnssec-verifier` | rien |
Pourquoi la clé ne naît pas sur la machine : une reconstruction depuis zéro signerait avec
une **autre** clé, le DS du registraire ne correspondrait plus, et le domaine deviendrait
**BOGUS** pour tout résolveur validant, sans qu'aucune machine soit en panne.
Trois règles, chacune mesurée :
- **Changer la clé ou retirer la signature pendant qu'un DS est publié est refusé** par l'outil
de la machine (`setops-dnssec-zone`) comme par `dnssec.py`. Une mesure du DS qui échoue vaut
refus.
- **Le serial servi est l'heure (`SOA-EDIT EPOCH`).** PowerDNS renouvelle ses signatures chaque
semaine (inception au début de la semaine précédente, expiration deux semaines après le
début de la semaine courante). `INCEPTION-EPOCH`, essayé d'abord, ne dépassait notre serial
qu'au moment où les signatures du site expiraient. Avec l'heure, le site retire la zone à
chaque rafraîchissement ; les signatures servies gardent 7 à 14 jours.
- **L'état DNSSEC fait partie du corps de la zone.** Signer ne change aucun enregistrement :
sans la ligne `; DNSSEC : …`, le serial n'aurait pas bougé et le site aurait gardé la version
non signée.
La sonde `zones-publiques` du site alerte sur une zone signée servie **nue**, avertit sous
6 jours de signature restante, et alerte sous 3.
**Ordre de la mise en service** : basculer les serveurs de noms (zone signée, sans DS : le
domaine reste « non sécurisé », pas cassé), vérifier, **puis** remettre le DS au registraire.
Jamais l'inverse.

View file

@ -405,7 +405,7 @@ en fonctionnement. L'état réel :
| Les `meta/acces.yml` | **écrits pour les cinq services `web-sso`** : `serveur_forgejo`, `serveur_grafana`, `serveur_icingaweb2`, `serveur_keycloak`, `serveur_nextcloud` |
| La preuve | **écrite le 2026-09-07 — c'est P58** |
Le §5 de [`authentification.md`](authentification.md) formule la règle : *une directive
Le §5 de [`authentification.md`](../conception/authentification.md) formule la règle : *une directive
qu'aucune garde ne vérifie finit par ne plus être vraie*. `meta/acces.yml` a été dans ce cas
jusqu'au 2026-09-07 : rien ne vérifiait qu'un service `web-sso` en porte un. **P29** gardait
les *positions* d'authentification ; **P58** garde désormais les *habilitations*.

View file

@ -22,7 +22,7 @@ la connexion au cluster Proxmox et les valeurs de clonage par défaut. Il pose
sans tout retaper.
> Ces paramètres sont des **intrants communs** de l'écosystème (cf.
> [`intrants-communs.md`](intrants-communs.md) §C). L'accès au cluster et le golden
> [`intrants-communs.md`](../conception/intrants-communs.md) §C). L'accès au cluster et le golden
> template sont des **constantes** ; le placement par défaut est **surchargeable
> par hôte** dans `instance/plan/serveurs.yml`.
@ -77,7 +77,7 @@ par écosystème — fini les voûtes éparpillées.
> et le `Makefile` les rassemble dans `ANSIBLE_VAULT_IDENTITY_LIST` via
> `scripts/voutes.py`. Créer une VM ouvre d'ailleurs **deux** voûtes dans la même
> exécution : celle du tenant, et celle de l'hébergeur qui détient le jeton Proxmox. Gabarit committé :
[`exemples/vault.exemple.yml`](../exemples/vault.exemple.yml) (les deux clés du token
[`exemples/vault.exemple.yml`](../../exemples/vault.exemple.yml) (les deux clés du token
Proxmox + **15 clés `vault_*`** pour PKI, LDAP/SSO, bases, forge, observabilité).
L'assistant demande « Configurer la voûte de secrets maintenant ». Si `oui` :
@ -159,7 +159,7 @@ Dans l'interface Proxmox (ou en CLI `pveum`) :
## Voir aussi
- [`intrants-communs.md`](intrants-communs.md) — tous les intrants communs de l'écosystème.
- [`nomenclature-vm.md`](nomenclature-vm.md) — réseau, VLAN, fonctions.
- [`intrants-communs.md`](../conception/intrants-communs.md) — tous les intrants communs de l'écosystème.
- [`nomenclature-vm.md`](../conception/nomenclature-vm.md) — réseau, VLAN, fonctions.
- [`vm-lifecycle.md`](vm-lifecycle.md) — cycle de vie d'une VM (création → conformité).
- `make` — liste des cibles (l'aide est la cible par défaut).

View file

@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
## Le trou qu'il comble
`scripts/prouver.py` porte 83 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
`scripts/prouver.py` porte 95 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
propriétaire, que rien n'est codé en dur.
@ -137,7 +137,7 @@ Reste un écart de nature différente, et le devis le nomme comme tel : *livré
livré* = la bordure autorise et l'hôte refuse. Rien ne fuit, mais les deux couches ne
déclarent pas la même politique — et une politique qu'on ne peut plus lire est une politique
qu'on ne tient plus. C'est ce qui a fait naître le mot-clé `poste: false` dans
`meta/flux.yml` (voir [`frontiere-opnsense.md`](frontiere-opnsense.md)).
`meta/flux.yml` (voir [`frontiere-opnsense.md`](../conception/frontiere-opnsense.md)).
## Le devis des expositions

View file

@ -13,7 +13,7 @@ Le template doit rester un socle commun, clonable et sécuritaire. Il ne doit pa
L'état de départ attendu est décrit dans :
```text
docs/procedure-template-debian13-proxmox.md
docs/exploitation/procedure-template-debian13-proxmox.md
```
Résumé :
@ -326,11 +326,11 @@ clés privées
Les intégrations futures sont documentées dans :
```text
docs/integrations-vm.md
docs/conception/integrations-vm.md
```
Le cycle complet VM vanille, golden template, clone et conformité continue est documenté dans :
```text
docs/vm-lifecycle.md
docs/exploitation/vm-lifecycle.md
```

View file

@ -13,7 +13,7 @@ Ce document s’arrête au point de bascule vers le `Makefile`.
Les ajouts appliqués ensuite par Ansible, leur validation et leur justification sont documentés dans :
```text
docs/modeles_vm/debian13-proxmox.md
docs/exploitation/gabarit-debian13-justification.md
```
---

View file

@ -58,7 +58,7 @@ Mapping (défaut du rôle grafana) : `grafana-admin`→Admin, `grafana-editor`
> `personnel ⇒ Viewer`. Ajouter quelqu'un au **groupe** lui ouvre Grafana, Forgejo, Icinga et
> le courriel d'un seul geste — alors qu'une assignation nominative crée une dette qu'on
> découvre le jour du départ, service par service. L'assignation explicite ci-dessus reste
> le geste de dépannage, pas la façon normale d'accorder un accès. Voir `docs/autorisation.md` §5.
> le geste de dépannage, pas la façon normale d'accorder un accès. Voir `docs/exploitation/autorisation.md` §5.
---
@ -112,6 +112,64 @@ Note : ne s'applique qu'aux Forgejo **gérées par Set-OPS**.
## 5. Restaurer — et d'abord : prouver qu'on peut
### 5.0 La reconstruction remet l'état (depuis le 2026-09-30)
Jusqu'au 2026-09-30, **une reconstruction repartait d'un état neuf** : nouvelle racine
d'AC, annuaire vierge (`sysadmin` au mot de passe d'amorçage), bases, Nextcloud et courriel
vides. Les instantanés se restauraient pour *prouver* qu'ils s'ouvrent, jamais pour être
remis en service.
Désormais **chaque rôle propriétaire remet l'état de l'incarnation précédente**, au moment
où il le créerait neuf — la bonne séquence vient des couches de déploiement :
| Rôle | Quand | Condition « vierge » |
|---|---|---|
| `serveur_step_ca` | avant `step ca init` | pas de `ca.json` — et la voûte doit ouvrir les clés restaurées |
| `serveur_postgresql` | juste après la création des bases | base sans table (hors `nextcloud`, `icinga`) |
| `serveur_openldap` | en fin de rôle (schéma et `ppolicy` chargés) | aucune entrée hors la racine |
| `serveur_dovecot` | après le répertoire des boîtes | répertoire vide |
| `serveur_rspamd` | avant la génération DKIM | pas de clé DKIM |
| `serveur_forgejo` | avant l'arborescence de données | répertoire vide |
| `serveur_nextcloud` | **en fin de rôle** : base + fichiers + config ensemble, puis `occ upgrade` | pas de `config.php` au départ, base vide |
| `serveur_web_dorsal` | après sa racine | racine vide |
**L'instantané remis** est le dernier pris **avant la naissance de la machine** (date de
sa clé d'hôte SSH). Un état en place n'est jamais écrasé d'office. Chaque jeu laisse un
marqueur dans `/etc/setops/restauration/<jeu>` ; ce qui a été remplacé est mis de côté dans
`/var/backups/setops-avant-restauration/`.
**La sauvegarde refuse de déposer** (code 3) tant qu'un état d'avant n'a été ni remis ni
écarté : sinon `restic forget --keep-daily` chasserait l'instantané d'avant au profit de
l'état neuf du même jour.
**L'instantané d'avant rasage est étiqueté** `avant-raser` (depuis le 2026-10-07) et la
rétention le garde **jusqu'à la reconstruction suivante**, qui lui retire l'étiquette. Sans
elle, le premier dépôt de la machine reconstruite le chassait le jour même : après la
reconstruction de Technolibre (M4), il ne restait rien à quoi comparer l'état remis.
**Les témoins** comparent l'état vivant à cet instantané. Un écart, c'est une **perte**
(fichier, courriel, entrée de l'annuaire, base, rôle, table, ligne d'historique, ou une
base dont plus aucune ligne d'avant ne subsiste) ou une **identité changée** : clés et
certificats de l'AC, AC d'Icinga et environnement d'Icinga DB, clé DKIM, `instanceid` de
Nextcloud, mot de passe d'une entrée de l'annuaire. PostgreSQL se compare par clé (première
colonne de chaque table), pas par nombre de lignes. Le reste (un courriel reçu depuis, une table de sessions, le bayes de rspamd,
un cache) est listé sans être un écart. La reconstruction les lance à son étape `bilan`.
Les gestes :
```
make sauvegarder-maintenant # JUSTE AVANT de raser : étiqueté « avant-raser »
make restauration-etat [HOTE=...] # ce que chaque jeu est devenu, et si son instantané est encore au dépôt
make temoins-etat [HOTE=...] # l'état vivant contre l'instantané d'avant rasage
make temoins-etat SOURCE=candidat # faute d'étiquette : contre le dernier d'avant la naissance
make restauration-renoncer HOTE=.. JEU=.. CONFIRMER=true # écarter un état, sans le remettre
-e client_backup_restauration_instantane=<id> # imposer un instantané, par hôte
```
Sur un nœud, sans Ansible : `setops-restaurer etat`, `setops-restaurer temoins`, et les répétitions qui ne touchent à
rien — `setops-restaurer annuaire --essai <base_dn>`, `base <nom> --vers epreuve`,
`fichiers --vers /var/tmp/epreuve <chemin>`.
> Éprouvé le 2026-08-12 sur Chezlepro. `make valider` rejoue la partie automatisable ;
> la restauration d'une base reste manuelle, et porte un piège décrit plus bas.
@ -143,10 +201,11 @@ la base badger de step-ca, qui avance à **chaque** émission de certificat.
`pg_dumpall` écrit `CREATE DATABASE <suivante>` **avant** le `\connect` correspondant.
Découper « du `\connect X` au `\connect` suivant » emporte donc un ordre qui vise une
**autre** base. Couper aussi sur `CREATE DATABASE`, et **vérifier avant de rejouer** :
**autre** base. Couper aussi sur `CREATE DATABASE` et sur `DROP DATABASE` (avec `--clean`,
la section de `nextcloud` finissait par `DROP DATABASE postgres;` — 2026-09-30), et **vérifier avant de rejouer** :
```
awk '/^\\connect forgejo$/{f=1;next} f && (/^\\connect /||/^CREATE DATABASE /){exit} f' \
awk '/^\\connect forgejo$/{f=1;next} f && (/^\\connect /||/^CREATE DATABASE /||/^DROP DATABASE /){exit} f' \
toutes-bases.sql > section.sql
grep -qE '^(DROP|CREATE|ALTER) DATABASE|^\\connect' section.sql \

View file

@ -15,7 +15,10 @@ ce poste et sur le runner de son écosystème, qui en reçoit une copie chiffré
d'une seconde couche ; le script refuse tout fichier dont l'en-tête n'est pas
`$ANSIBLE_VAULT`, garde le chemin de chaque voûte (elles s'appellent presque toutes
`vault.yml`) et relit l'archive empreinte par empreinte. Restauration :
`tar -xf setops-voutes-*.tar -C <dossier des dépôts>`. **À refaire après toute écriture
`tar -xf "$(ls -t setops-voutes-*.tar | head -1)" -C <dossier des dépôts>` (la plus récente).
Chaque export porte sa date (`setops-voutes-<date>-<poste>.tar`, l'heure en plus au second
export du jour) et n'écrit rien si les voûtes n'ont pas changé depuis la dernière archive.
**À refaire après toute écriture
dans une voûte**, comme les clés après toute voûte nouvelle.
> **Ce chiffre était trois, et il a baissé sans que rien ne le signale.** Un coffre répliqué deux fois reste
@ -77,7 +80,7 @@ Une procédure rangée dans le dépôt serait donc inaccessible exactement quand
La clé se suffit donc à elle-même. À côté de l'archive :
```
setops-cles-<poste>.tar.gpg les clés, chiffrées
setops-cles-<date>-<poste>.tar.gpg les clés, chiffrées (une archive datée par export)
restaurer_cles.py le script, autonome
LISEZ-MOI-RESTAURATION.txt le mode d'emploi, et les commandes manuelles
```
@ -89,8 +92,9 @@ LISEZ-MOI-RESTAURATION.txt le mode d'emploi, et les commandes manuelles
Sur la machine neuve, il ne faut que `gpg`, `python3` et la phrase de passe :
```bash
python3 restaurer_cles.py setops-cles-*.tar.gpg --lister # voir sans rien écrire
python3 restaurer_cles.py setops-cles-*.tar.gpg # remettre en place
A="$(ls -t setops-cles-*.tar* | head -1)" # la plus récente
python3 restaurer_cles.py "$A" --lister # voir sans rien écrire
python3 restaurer_cles.py "$A" # remettre en place
```
Le script remet chaque fichier à sa place selon son nom, et **repose les droits à 0600**.

View file

@ -63,7 +63,7 @@ nftables préparé mais désactivé
Les détails et justifications sont dans :
```text
docs/modeles_vm/debian13-proxmox.md
docs/exploitation/gabarit-debian13-justification.md
```
## 3. Validation et cleanup
@ -249,5 +249,5 @@ visualisation
Elles sont documentées dans :
```text
docs/integrations-vm.md
docs/conception/integrations-vm.md
```

View file

@ -36,7 +36,7 @@ par une **commande qui interroge le système**, jamais par une conviction.
- [ ] **Créer le dépôt de l'hébergeur** — un dossier frère, avec deux fichiers :
`underlay.yml` et `proxmox-hebergeur.yml` (squelettes en annexe).
- [ ] **Emporter le gabarit** `modeleSetOPS` sur disque (`vzdump`), *et* la procédure de
fabrication en repli : [`procedure-template-debian13-proxmox.md`](procedure-template-debian13-proxmox.md).
fabrication en repli : [`procedure-template-debian13-proxmox.md`](../exploitation/procedure-template-debian13-proxmox.md).
- [ ] **Préparer la voûte** de l'instance à recevoir les deux paires de secrets
(hyperviseur, frontière). Jamais dans un dépôt git, jamais dans une conversation.
- [ ] **Vérifier la version de Proxmox.** Tout ce dépôt a été éprouvé sur PVE 8. Sur PVE 9,
@ -174,7 +174,7 @@ make frontiere-plan make sdn-plan make placement-plan
- [ ] **La sauvegarde emporte-t-elle quelque chose ?** Compter les fichiers, pas les
succès. Une sauvegarde verte sur zéro fichier a tenu six semaines ici.
- [ ] **Éprouver une restauration** — [`runbooks-exploitation.md`](runbooks-exploitation.md) §5.
- [ ] **Éprouver une restauration** — [`runbooks-exploitation.md`](../exploitation/runbooks-exploitation.md) §5.
Une sauvegarde jamais restaurée n'est pas une sauvegarde.
- [ ] **La supervision voit-elle l'unité de sauvegarde ?** Sans
`vault_icinga_api_depot`, personne ne surveille les sauvegardes — et rien ne le dit.
@ -267,6 +267,6 @@ grosses meurent, et rien n'est signalé. `make mtu-mesurer` tranche.
- [`preparer-un-site-hebergeur.md`](preparer-un-site-hebergeur.md) — ce que l'hébergeur prépare.
- [`migration-tenant.md`](migration-tenant.md) — déplacer un tenant **vivant**.
- [`multi-instances.md`](multi-instances.md) — un moteur, N écosystèmes.
- [`sdn-evpn.md`](sdn-evpn.md) — pourquoi une zone EVPN par tenant.
- [`decisions-architecture.md`](decisions-architecture.md) — D-71 (PKI/DNS d'abord), D-77, D-78, D-80.
- [`multi-instances.md`](../conception/multi-instances.md) — un moteur, N écosystèmes.
- [`sdn-evpn.md`](../conception/sdn-evpn.md) — pourquoi une zone EVPN par tenant.
- [`decisions-architecture.md`](../projet/decisions-architecture.md) — D-71 (PKI/DNS d'abord), D-77, D-78, D-80.

View file

@ -4,8 +4,8 @@
> **Recette d'exploitation.** Elle décrit la séquence, les états et les gardes ; elle ne
> décrit pas encore un outil, parce que la séquence doit être éprouvée avant d'être figée
> dans du code. Complète `docs/multi-instances.md` (la fédération) et
> `docs/frontiere-opnsense.md` §2 (qui possède quoi).
> dans du code. Complète `docs/conception/multi-instances.md` (la fédération) et
> `docs/conception/frontiere-opnsense.md` §2 (qui possède quoi).
## 1. Pourquoi ce document existe

View file

@ -169,7 +169,7 @@ un artefact du **moteur**, pas d'un tenant. Une VM Debian minimale avec
> C'est arrivé, et il a fallu recapturer le gabarit puis reconstruire pour s'en défaire.
En cas de doute, laisser l'exploitant le fabriquer : c'est une demi-heure, et la procédure
est écrite (`docs/procedure-template-debian13-proxmox.md`).
est écrite (`docs/exploitation/procedure-template-debian13-proxmox.md`).
## 7. Ce que l'exploitant doit recevoir
@ -252,7 +252,7 @@ réseaux **défait en silence l'isolation inter-tenant**. WireGuard s'y prête b
## Voir aussi
- [`sdn-evpn.md`](sdn-evpn.md) — pourquoi une zone EVPN par tenant.
- [`config-proxmox.md`](config-proxmox.md) — les intrants côté hyperviseur, et le token.
- [`frontiere-opnsense.md`](frontiere-opnsense.md) — la frontière, hors flotte Ansible.
- [`procedure-template-debian13-proxmox.md`](procedure-template-debian13-proxmox.md) — fabriquer `modeleSetOPS`.
- [`sdn-evpn.md`](../conception/sdn-evpn.md) — pourquoi une zone EVPN par tenant.
- [`config-proxmox.md`](../exploitation/config-proxmox.md) — les intrants côté hyperviseur, et le token.
- [`frontiere-opnsense.md`](../conception/frontiere-opnsense.md) — la frontière, hors flotte Ansible.
- [`procedure-template-debian13-proxmox.md`](../exploitation/procedure-template-debian13-proxmox.md) — fabriquer `modeleSetOPS`.

View file

@ -99,7 +99,7 @@ Nommer un point ouvert vaut mieux qu'une ligne rassurante sans mécanisme derri
Aucune de ces responsabilités n'est laissée à la parole :
```
make prouver le dépôt est-il cohérent avec lui-même (83 preuves, zéro réseau)
make prouver le dépôt est-il cohérent avec lui-même (95 preuves, zéro réseau)
make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80)
make certificats-plan ce que le disque porte contre ce que la mémoire sert
make expositions-plan chaque service publié répond-il, et depuis où

View file

@ -19,8 +19,8 @@
> l'« incident récent » et son correctif. Les deux `handlers/main.yml` existent
> (`ssh_baseline`, `ssh_hardening`) depuis longtemps.
>
> **Où aller à la place** : `AGENTS.md` (autorité), `docs/carte-set-ops.md` (par où entrer
> pour modifier le moteur), `docs/catalogue-services.md` (ce qui est éprouvé et ce qui ne
> **Où aller à la place** : `AGENTS.md` (autorité), `docs/README.md` (l'index) et
> `docs/conception/mecanismes.md` (par où entrer pour modifier le moteur), `docs/projet/catalogue-services.md` (ce qui est éprouvé et ce qui ne
> l'est pas).
## État du dépôt *(juillet 2026)*

View file

@ -15,11 +15,11 @@ rôles** de durcissement, et dans cet ordre — `hardening_packages`, `sysctl_ha
`core_dumps`, `unattended_upgrades`, `apparmor`, `auditd`, `fail2ban_ssh`, `journald`,
`ssh_hardening`, `nftables_baseline` — plutôt qu'un rôle homonyme.
La nomenclature des VM et des VMID est documentée dans `docs/nomenclature-vm.md`.
La nomenclature des VM et des VMID est documentée dans `docs/conception/nomenclature-vm.md`.
Un service central peut partager un hôte avec d'autres services de la même fonction opérationnelle (plusieurs applications par VM). La relation stricte est entre groupe et playbook, pas entre groupe et VM.
> Modèle à jour : chaque service est une **application** (`instance/plan/applications.yml`). Voir `docs/plan-et-generation.md`.
> Modèle à jour : chaque service est une **application** (`instance/plan/applications.yml`). Voir `docs/conception/plan-et-generation.md`.
## État d'implémentation des rôles
@ -66,6 +66,7 @@ Ce que la reconstruction couvre, par capacité :
| Resolveur du site | `serveur_resolveur_site` | designe LE resolveur que les ecosystemes de ce site interrogent tant qu'ils n'ont pas le leur. Marqueur : `serveur_resolveur` installe. |
| Depot de sauvegarde du site | `serveur_backup_site` | designe LE depot ou les ecosystemes de ce site posent leur etat tant qu'ils n'ont pas le leur. Marqueur : `serveur_backup` installe. |
| DNS public du site | `serveur_dns_public` | **secondaire** public de toutes les zones `autorite: primaire-cache` des locataires du site : le locataire ecrit, le site sert. Transfert signe TSIG, aucune adresse de confiance, aucune zone `.internal`. Phase 1 : non expose a Internet |
| Relais de la face publique | `serveur_relais` | un VPS **hors du site** qui loue ses adresses fixes quand le site n'a qu'une adresse DHCP : il repond a l'ARP de son hebergeur pour elles (proxy) et les **route** en /32 dans un tunnel WireGuard que la frontiere initie. Aucune traduction : l'adresse du client arrive intacte. Inventorie depuis la carte, comme un hyperviseur |
| Agents de flotte | `client_metrique`, `client_journal`, `client_smtp`, `client_sante` | collecte, relais et rapport de santé sur toute la flotte |
*Rôles retirés (2026-07-04, supersédés ou hors conception)* : `serveur_sendmail`
@ -201,7 +202,7 @@ dans le plan.
## Ordre de déploiement — le raisonnement
> **L'ordre exécutable n'est pas ici.** Il vit dans `docs/couches-deploiement.yml` et
> `docs/dependances-groupes.yml`, que P08 prouve cohérents (42 groupes classés, aucun
> `docs/dependances-groupes.yml`, que P08 prouve cohérents (43 groupes classés, aucun
> cycle, aucune arête en arrière) et que `make reconstruire` suit. Ce qui suit en est le
> **raisonnement**, utile pour comprendre pourquoi cet ordre-là — et pour placer un
> service nouveau. Les phases sont franchies : les « intégrations à prévoir » ci-dessous
@ -226,7 +227,7 @@ L'ordre ci-dessous privilégie les dépendances structurantes avant les applicat
est une couche distincte (`serveur_resolveur` / `client_resolveur`, Unbound), et le
**plancher `/etc/hosts`** posé par `hosts_statiques` précède les deux — c'est lui qui
permet à l'écosystème de se résoudre DNS éteint. Trois couches, pas un choix de design
(`docs/dns-interne.md`).
(`docs/conception/dns-interne.md`).
- Raison : les autres intégrations auront besoin de noms stables plutôt que d'adresses IP.
2. `serveur_step_ca`
@ -324,7 +325,7 @@ Les groupes clients peuvent être ajoutés aux VM existantes quand le service ce
Les dépendances exécutables sont déclarées dans `docs/dependances-groupes.yml`.
Le runbook DNS initial est dans `docs/dns-interne.md`.
Le runbook DNS initial est dans `docs/conception/dns-interne.md`.
Ce fichier sert à deux usages :

View file

@ -55,7 +55,7 @@ sont les seules vérifiables.
| # | Décision | Pourquoi | Détail | Garde |
|---|---|---|---|---|
| **D-82** | **Patient 0 n'est le parent de personne.** Il est la **mise en œuvre de référence** du modèle `origine` — le plus petit écosystème complet — et un pair de la famille du génome, pas sa racine | trois faits l'ont retiré un par un : D-81 a donné l'autorité du génome à la forge du SITE (son dernier lecteur corrigé le 2026-08-26, Technolibre le 08-31) ; le dénominateur commun vit dans les modèles depuis le 08-24 ; et **l'ancêtre était locataire de son enfant** — index 29 sur la fabric de `SITE-Chezlepro`, qui descend de lui. `eregion` (`forge.alliance-boreale.ca`) n'est PAS sur le chemin du génome : le poste porte les commits en bundle au runner du site (`make genome-pousser`), qui pousse sur sa forge. `eregion` est une forge héritée, porte publique des contributions, qui ne fera jamais partie de Set-OPS — la redondance vient d'une forge par site, chacune inséminée du génome *(corrigé le 2026-09-28 : cette ligne l'avait dite « SPOF promu », sans mesurer le chemin)* | `OPS-Patient0/README.md`, `docs/filiation-emancipation.md` | — |
| **D-82** | **Patient 0 n'est le parent de personne.** Il est la **mise en œuvre de référence** du modèle `origine` — le plus petit écosystème complet — et un pair de la famille du génome, pas sa racine | trois faits l'ont retiré un par un : D-81 a donné l'autorité du génome à la forge du SITE (son dernier lecteur corrigé le 2026-08-26, Technolibre le 08-31) ; le dénominateur commun vit dans les modèles depuis le 08-24 ; et **l'ancêtre était locataire de son enfant** — index 29 sur la fabric de `SITE-Chezlepro`, qui descend de lui. `eregion` (`forge.alliance-boreale.ca`) n'est PAS sur le chemin du génome : le poste porte les commits en bundle au runner du site (`make genome-pousser`), qui pousse sur sa forge. `eregion` est une forge héritée, porte publique des contributions, qui ne fera jamais partie de Set-OPS — la redondance vient d'une forge par site, chacune inséminée du génome *(corrigé le 2026-09-28 : cette ligne l'avait dite « SPOF promu », sans mesurer le chemin)* | `OPS-Patient0/README.md`, `docs/conception/filiation-emancipation.md` | — |
| **D-83** | **Patient 0 a été retiré** — ses machines n'existent plus (constaté le 2026-09-06) | D-82 lui avait laissé une raison d'être : la mise en œuvre de référence du modèle `origine`, et un **témoin** de plus du génome. Le retrait solde les deux : l'une vit dans le modèle `origine`, l'autre revient aux forges de site. **Son plan a été effacé le 2026-09-27** : l'index 29 est libéré, et le site n'ouvre plus rien à `10.29.0.0/16` | `SITE-Chezlepro/underlay.yml` (`tenants`), `SITE-Chezlepro/flux-genere/` | **P21** (index), **P23** |
| **D-84** | **Le plan de contrôle reste gelé — c'est la CARTE DES SEUILS qui était fausse** | La question « et si on retirait le gel ? » a mis à l'épreuve les cinq seuils de `positionnement.md`, et deux ne tenaient pas. **RBAC** : couvert depuis que trois classes d'acteurs aux pouvoirs disjoints existent — poste, runner de site, runners de tenant — séparés **cryptographiquement** (une voûte, une clé, 2026-08-28) et non par une table de permissions qu'une faille applicative contournerait ; adopter AWX pour ce besoin serait **régresser**. **IPAM** : sans objet par construction — rien ne s'alloue, tout dérive du seed, et P20/P21/P23/P28/P33 tiennent déjà ce qu'un IPAM vérifierait *a posteriori*. Les deux lignes sont retirées du tableau : les garder aurait fait adopter un outil pour un besoin déjà rempli. **Et un seuil manquait** — l'**émancipation** : le GUI est mono-utilisateur (`127.0.0.1` + jeton), or la trajectoire mène à plusieurs humains aux portées disjointes, sur des machines qui ne sont pas les nôtres. Ce seuil n'appelle pas AWX, il appelle une décision non prise. *Un seuil qu'on ne nomme pas est un seuil qu'on franchit sans le voir.* Corollaire consigné : le gel porte sur les **fonctions**, jamais sur les **vues** — montrer à l'écran ce que le moteur sait déjà ne franchit aucun seuil | `positionnement.md` §3, §4, §5 | — |
| **D-85** | **cloud-init naît avec la VM et ne lui survit pas** | cloud-init n'est pas un logiciel d'installation : c'est une **source de vérité externe**, qui se réveille à *chaque* démarrage et relit le lecteur attaché par l'hyperviseur — lequel peut redéfinir comptes, clés SSH autorisées, mots de passe et réseau. Sur une machine que le plan possède, c'est un **second maître** : le plan ne le décrit pas, `make valider` ne le mesure pas, et il parle en premier. Sa tâche est pourtant finie à la première seconde — c'est parce qu'il a **réussi** à poser l'adresse et les clés qu'Ansible a pu entrer. **Trois moitiés, qui se défont séparément** : le **gabarit** le garde (sans lui un clone ne naît pas — P56) ; le **socle** ne l'installe plus (le garder produisait un va-et-vient à chaque déploiement : le socle installe, le durcissement retire, deux `changed` par passage) ; le **durcissement** le retire (`cloud_init_retrait`, en dernier). **Ce qui rend le retrait sûr est mesuré, pas supposé** (2026-09-09, `obs-01`) : `/etc/network/interfaces.d/50-cloud-init` n'appartient à aucun paquet — `dpkg -S` ne le trouve pas — et le `postrm` ne le nomme jamais, même en `purge`. L'adresse survit. Le rôle le **vérifie** malgré tout, avant et après : une VM qui perd ce fichier ne se plaint pas, elle repart sans adresse et plus personne ne peut entrer pour le constater. **⚠ CE QUE CETTE DÉCISION NE FERME PAS — et il faut le dire, sinon elle se lit comme une émancipation qu'elle n'est pas.** Retirer cloud-init **n'ôte aucun pouvoir à l'hébergeur**. `qemu-guest-agent` est au gabarit (P56 : il doit y être — c'est par lui que `creer-vm` confirme la matérialisation sans entrer chez le tenant), et l'API Proxmox expose sur son dos, sur toute VM vivante de la flotte, un pouvoir **strictement plus grand** que le lecteur cloud-init : `exec`, `file-write`, `file-read`, `set-user-password`, `shutdown` (relevé le 2026-09-09 sur `edge-mta-01`, jeton d'API du site). Ce que D-85 ferme est donc **précis et étroit** : (a) une réapplication **automatique, à chaque démarrage**, depuis un support que le plan ne possède pas et qu'aucune preuve ne lit ; (b) le code de cloud-init lui-même — un interpréteur Python complet, exécuté en root au démarrage, et ses ~29 dépendances. Elle ne ferme **pas** la mainmise de l'hyperviseur sur ses invités : celle-là est une propriété de la virtualisation, pas de cloud-init, et elle appelle sa propre décision — non prise. **Le seuil où le remplacer deviendrait juste** : le jour où une première seconde ne peut plus être amorcée par Proxmox (autre hyperviseur, métal nu, hébergeur sans API), le chemin par l'agent invite cesse d'être une réimplémentation d'un standard — que `positionnement.md` interdit — et devient **le chemin portable**. Tant que ce seuil n'est pas atteint, écrire soi-même l'amorçage serait échanger un standard éprouvé contre du code maison au moment le plus fragile, dont le mode de panne est le pire : une VM injoignable | `roles/cloud_init_retrait/`, `serveur_durci.yml`, `positionnement.md` | **P63** |
@ -121,14 +121,14 @@ sont les seules vérifiables.
| **D-73** | Tout port **lié** se déclare, et deux rôles co-localisés ne peuvent pas revendiquer le même | un port n'appartient à personne : le premier démarré le prend, l'autre échoue — parfois **en silence**. Sur `infra-mail-01`, le SASL de Dovecot et l'interface d'Alloy se disputaient le 12345 depuis le premier jour, et c'est Dovecot qui perdait sans que rien ne le dise. Le contrôle n'était possible qu'une fois le port d'Alloy — un défaut amont **subi** — déclaré. `partage: true` distingue « j'ouvre cette écoute » de « je décris celle d'un autre » | `scripts/verifier_ports.py`, `make ports-verifier` | **P33** |
| **D-70** | La documentation **dit et explique tout ce que le dépôt fait** — et l'exigence est **outillée**, pas seulement énoncée | une exigence qu'on n'outille pas pourrit en silence : la carte annonçait « 28 décisions » quand il y en avait 66, et disait les accès « non construits » alors qu'ils tournaient en production. **P31** garde le couvert — chaque script s'explique et reste atteignable, chaque cible `make` porte son aide (sauf les internes préfixées `_`), chaque rôle a son README. Elle ne garde **pas** la qualité du « pourquoi » : ça se juge en revue, et ça vit dans `CHANGELOG.md` et ici | `devis-services.md`, `CHANGELOG.md` | **P31** |
| **D-75** | Un ecart **lisible dans le plan** se prouve dans le plan, pas au deploiement | l'assert de `resoudre_base` fait son travail — mais il s'est declenche a la 92e tache de `collab-01`, apres quarante minutes, pour une application declaree sans base au registre. L'information etait entierement statique. **P35** derive les roles qui exigent une base (ceux qui incluent `resoudre_base`) et le groupe qu'ils reclament (le DEFAUT de la variable passee, jamais leur nom : `serveur_icingaweb2` reclame la base d'`serveur_icinga`). Meme raisonnement que **D-72**/P32 pour les intrants : une garde de deploiement gagne a etre doublee d'une preuve statique | `scripts/prouver.py`, `plan/bases-donnees.yml` | **P35** |
| **D-74** | **Chaque document déclare son lecteur** en tête — un lecteur et sa situation, pas une catégorie de sujet | la documentation était rangée par SUJET, ce qui est juste pour de la référence — mais personne n'arrive avec un sujet, on arrive avec une **situation**. Symptôme exact : `autorisation.md` portait le runbook de reprise le plus utile du dépôt, enfoui au §6, parce que son sujet est l'autorisation ; personne n'allait l'y chercher. Un document qui déclare son lecteur se range tout seul, et un intrus s'y voit. Deux exemptions, **dérivées et non listées** : un document qui s'annonce généré, et un fragment sans titre | `README.md`, `wiki/Reprendre-l-écosystème.md`, `docs/carte-set-ops.md` | **P34** |
| **D-74** | **Chaque document déclare son lecteur** en tête — un lecteur et sa situation, pas une catégorie de sujet | la documentation était rangée par SUJET, ce qui est juste pour de la référence — mais personne n'arrive avec un sujet, on arrive avec une **situation**. Symptôme exact : `autorisation.md` portait le runbook de reprise le plus utile du dépôt, enfoui au §6, parce que son sujet est l'autorisation ; personne n'allait l'y chercher. Un document qui déclare son lecteur se range tout seul, et un intrus s'y voit. Deux exemptions, **dérivées et non listées** : un document qui s'annonce généré, et un fragment sans titre | `docs/README.md`, `wiki/Reprendre-l-écosystème.md` | **P34** |
| **D-71** | **Une PKI et un DNS fonctionnels avant toute chose** ; puis, par VM : socle → enrôlement PKI → enregistrement DNS (A **et** PTR) | `deployer-tout` déroule par COUCHES — correct, mais chaque VM réclame alors un certificat à une autorité pas encore debout, et l'échec se lit comme un défaut du rôle et non d'ordre. Les deux hôtes d'amorçage se **dérivent** de `applications.<step_ca\|powerdns>.hote` : déplacer l'autorité déplace l'amorçage. **Deux exceptions structurelles assumées** — l'AC s'auto-signe, le DNS pose son propre enregistrement | `Makefile` `_amorcer-socle`, `scripts/socle_amorcage.py` | — |
| **D-76** | Le **gabarit doit se FABRIQUER par le dépôt**, pas se façonner à la main — décidée le 2026-08-12, **volontairement différée** après Technolibre | tout dérive d'un plan et se prouve ; le gabarit est la **seule pièce faite à la main** — 853 lignes de procédure manuelle — et il est en amont des quatorze VM. Le 2026-08-09 l'a démontré : l'ancien portait une **clé privée d'hôte SSH** et un `/etc/resolv.conf` figé, recopiés dans chaque clone, et il a fallu le recapturer. La preuve de reconstruction s'arrête donc un cran trop tôt : on reconstruit la flotte, pas ce dont elle est clonée. **Cible** : image cloud Debian officielle → signature vérifiée contre une empreinte épinglée (`verifier_signature.py`, déjà écrit) → import, réglages, conversion. **Coût honnête** : la recette doit être COMPARÉE au gabarit courant avant bascule — un réglage oublié serait hérité par les quatorze VM, et découvert loin de sa cause. **En attendant**, un gabarit se transporte d'un cluster à l'autre par `vzdump` / `qmrestore` | `docs/procedure-template-debian13-proxmox.md`, `scripts/verifier_signature.py` | — *(à construire)* |
| **D-76** | Le **gabarit doit se FABRIQUER par le dépôt**, pas se façonner à la main — décidée le 2026-08-12, **volontairement différée** après Technolibre | tout dérive d'un plan et se prouve ; le gabarit est la **seule pièce faite à la main** — 853 lignes de procédure manuelle — et il est en amont des quatorze VM. Le 2026-08-09 l'a démontré : l'ancien portait une **clé privée d'hôte SSH** et un `/etc/resolv.conf` figé, recopiés dans chaque clone, et il a fallu le recapturer. La preuve de reconstruction s'arrête donc un cran trop tôt : on reconstruit la flotte, pas ce dont elle est clonée. **Cible** : image cloud Debian officielle → signature vérifiée contre une empreinte épinglée (`verifier_signature.py`, déjà écrit) → import, réglages, conversion. **Coût honnête** : la recette doit être COMPARÉE au gabarit courant avant bascule — un réglage oublié serait hérité par les quatorze VM, et découvert loin de sa cause. **En attendant**, un gabarit se transporte d'un cluster à l'autre par `vzdump` / `qmrestore` | `docs/exploitation/procedure-template-debian13-proxmox.md`, `scripts/verifier_signature.py` | — *(à construire)* |
| **D-77** | **L'underlay d'un site dérive du même `index` que son tenant**, dans la **bande basse** `10.<index>.0–15.x` ; les réseaux de **stockage** en sortent et sont **identiques partout**, en `192.168.<vlan>.0/24` | tant qu'il n'existait qu'un site, `10.0.x.x` suffisait. Deux sites qui doivent se joindre — reprise mutuelle, sauvegardes croisées, exploitation à distance — **ne peuvent pas** porter les mêmes plages : chaque routeur croit que le réseau est chez lui. Un « numéro de site » a d'abord été proposé : **rejeté**, c'était un SECOND SEED à tenir et à synchroniser, alors que tout dérive déjà d'`index` (D-17). La bande basse est sûre **par la règle, pas par chance** : les zones valent `10.<index>.(15+categorie).0/24`, donc troisième octet ≥ 16 — les octets 0 à 15 ne sont jamais alloués. Les troisièmes et derniers octets de l'underlay actuel sont ainsi **préservés** : seul le deuxième change, et l'invariant `.1` (D-04) survit intact. Le **stockage** en est sorti parce qu'il est jumbo, non routé, et ne quitte jamais son site : il n'a aucun besoin d'être unique, et l'aligner sur le VLAN (`192.168.20.x` ↔ VLAN 20) fait dire son VLAN à l'adresse. Contrepartie assumée : ces réseaux ne pourront jamais traverser un lien inter-sites — sans conséquence, puisque ce qui voyage entre deux sites est l'**état**, par le dépôt de sauvegarde, pas le stockage bloc | `underlay.yml` (`index`), `scripts/underlay.py`, `docs/preparer-un-site-hebergeur.md` | **P23** |
| **D-77** | **L'underlay d'un site dérive du même `index` que son tenant**, dans la **bande basse** `10.<index>.0–15.x` ; les réseaux de **stockage** en sortent et sont **identiques partout**, en `192.168.<vlan>.0/24` | tant qu'il n'existait qu'un site, `10.0.x.x` suffisait. Deux sites qui doivent se joindre — reprise mutuelle, sauvegardes croisées, exploitation à distance — **ne peuvent pas** porter les mêmes plages : chaque routeur croit que le réseau est chez lui. Un « numéro de site » a d'abord été proposé : **rejeté**, c'était un SECOND SEED à tenir et à synchroniser, alors que tout dérive déjà d'`index` (D-17). La bande basse est sûre **par la règle, pas par chance** : les zones valent `10.<index>.(15+categorie).0/24`, donc troisième octet ≥ 16 — les octets 0 à 15 ne sont jamais alloués. Les troisièmes et derniers octets de l'underlay actuel sont ainsi **préservés** : seul le deuxième change, et l'invariant `.1` (D-04) survit intact. Le **stockage** en est sorti parce qu'il est jumbo, non routé, et ne quitte jamais son site : il n'a aucun besoin d'être unique, et l'aligner sur le VLAN (`192.168.20.x` ↔ VLAN 20) fait dire son VLAN à l'adresse. Contrepartie assumée : ces réseaux ne pourront jamais traverser un lien inter-sites — sans conséquence, puisque ce qui voyage entre deux sites est l'**état**, par le dépôt de sauvegarde, pas le stockage bloc | `underlay.yml` (`index`), `scripts/underlay.py`, `docs/hebergement/preparer-un-site-hebergeur.md` | **P23** |
| **D-80** | **Un tenant est agnostique de son underlay.** Les deux symlinks composent deux axes INDEPENDANTS : `instance` designe le tenant, `underlay.yml` la fabric qui l'accueille. Seule exception : une **liaison de placement** de trois cles vivantes | le commentaire disait « la fabric reste celle de l'hebergeur, quel que soit le tenant actif » — vrai, mais centre sur l'hebergeur. Le cadrage juste est centre sur le TENANT : tout son adressage derive du seed `index`, donc son plan se deplace d'une fabric a l'autre **sans y toucher**. Ce qui ne se deplace pas, c'est le **placement** : `proxmox_clone_noeud`, `_stockage` et le gabarit `_vmid_modele` — trois valeurs cote tenant, parce que c'est lui qui choisit ou se poser, mais qui NOMMENT des objets de l'hebergeur. Trois, pas trente : c'est ce qui separe « portable » de « theoriquement portable ». **Le PONT n'en fait pas partie** (mesure le 2026-08-13) : `instancier` pose `proxmox_pont` par hote, derive du VNet de sa zone. `proxmox_clone_pont` n'est qu'un REPLI pour un clone manuel hors flotte — d'ou son intitule dans le GUI, et d'ou le fait que `vmbr3` ait pu y rester faux dix jours | `underlay.py`, `inventories/*/group_vars/proxmox.yml`, `proxmox-hebergeur.yml` | **P37** |
| **D-79** | Le filtrage que l'hyperviseur applique aux VM tourne encore sur **iptables *legacy***. Le passer à `proxmox-firewall` (nftables natif) est une **dette à rembourser**, pas une option — et c'est le même geste qui rend le **pare-feu de VNet** opérant. Décidée le 2026-08-12, **différée après la reconstruction** | mesuré sur `asgard` : `iptables v1.8.9 (legacy)` — pas `(nf_tables)` ; `nftables v1.0.6` installé mais inutilisé par Proxmox ; `proxmox-firewall 0.7.1` **installé et absent des services en cours** ; seul `pve-firewall 5.1.3` tourne. Le pare-feu de VNet est **entièrement expressif** (`type/action/proto/source/dest/dport`, et `policy_forward` ∈ `ACCEPT,DROP` — donc un vrai default-deny inter-zone) : une règle d'essai a été créée, relue, puis retirée. **Mais il n'est implémenté QUE par le moteur nftables** : posée aujourd'hui, une règle serait acceptée, stockée, visible dans l'interface, et n'appliquerait rien. Trois gains dans le même geste — le moteur rejoint la doctrine (« tout est nftables »), le pare-feu de VNet devient réel, et ses règles **survivent à la reconstruction** là où les groupes par VM doivent être ré-attachés à chaque cycle | `devis_proxmox_fw.py`, `docs/sdn-evpn.md` | P25 *(sur le mécanisme actuel)* |
| **D-79** | Le filtrage que l'hyperviseur applique aux VM tourne encore sur **iptables *legacy***. Le passer à `proxmox-firewall` (nftables natif) est une **dette à rembourser**, pas une option — et c'est le même geste qui rend le **pare-feu de VNet** opérant. Décidée le 2026-08-12, **différée après la reconstruction** | mesuré sur `asgard` : `iptables v1.8.9 (legacy)` — pas `(nf_tables)` ; `nftables v1.0.6` installé mais inutilisé par Proxmox ; `proxmox-firewall 0.7.1` **installé et absent des services en cours** ; seul `pve-firewall 5.1.3` tourne. Le pare-feu de VNet est **entièrement expressif** (`type/action/proto/source/dest/dport`, et `policy_forward` ∈ `ACCEPT,DROP` — donc un vrai default-deny inter-zone) : une règle d'essai a été créée, relue, puis retirée. **Mais il n'est implémenté QUE par le moteur nftables** : posée aujourd'hui, une règle serait acceptée, stockée, visible dans l'interface, et n'appliquerait rien. Trois gains dans le même geste — le moteur rejoint la doctrine (« tout est nftables »), le pare-feu de VNet devient réel, et ses règles **survivent à la reconstruction** là où les groupes par VM doivent être ré-attachés à chaque cycle | `devis_proxmox_fw.py`, `docs/conception/sdn-evpn.md` | P25 *(sur le mécanisme actuel)* |
> **Ce qui n'a PAS été établi, et qu'il faudra éprouver.** `nft list tables` exige root et le
> compte `ansible` ne l'a pas sur les hyperviseurs : **le jeu de règles réellement chargé n'a
@ -147,7 +147,7 @@ sont les seules vérifiables.
> filtrage par VM tient toujours, puis poser un `policy_forward: DROP` sur un VNet et
> **prouver qu'il bloque, avec son contrôle négatif**.
| **D-78** | Un réseau d'underlay est soit une **destination**, soit un **chemin**. Les destinations gardent l'adressage dérivé (`10.<index>.0–15.x`) ; les chemins sortent de cet espace en **`192.168.<vlan>.0/24`, identiques à tous les sites** — décidée le 2026-08-12, **appliquée au déplacement physique** | D-77 sortait déjà le stockage, mais par une exception nommée plutôt que par une règle. Le critère n'est pas « y a-t-il des VM dedans » — le VLAN de gestion n'en a pas non plus — c'est : **ce réseau est-il jamais une destination, ou seulement un chemin ?** Transit (40) : rien que des prochains sauts, deux extrémités adjacentes en L2. Transport VXLAN (**VLAN 50**) : VTEP ↔ VTEP, destination de rien. Stockage (20/30/31) : baie ↔ hyperviseurs. **Gestion (10) : le poste de l'exploitant, un VPN, demain le lien inter-sites doivent l'atteindre** — elle reste dérivée et unique. Effet : sur six réseaux, **cinq** cessent d'exiger la moindre coordination entre deux hébergeurs, et « unique » redevient signifiant — seul ce qui doit l'être l'est | `underlay.yml`, `docs/preparer-un-site-hebergeur.md` | P23 *(la borne de bande basse ne s'applique qu'aux destinations)* |
| **D-78** | Un réseau d'underlay est soit une **destination**, soit un **chemin**. Les destinations gardent l'adressage dérivé (`10.<index>.0–15.x`) ; les chemins sortent de cet espace en **`192.168.<vlan>.0/24`, identiques à tous les sites** — décidée le 2026-08-12, **appliquée au déplacement physique** | D-77 sortait déjà le stockage, mais par une exception nommée plutôt que par une règle. Le critère n'est pas « y a-t-il des VM dedans » — le VLAN de gestion n'en a pas non plus — c'est : **ce réseau est-il jamais une destination, ou seulement un chemin ?** Transit (40) : rien que des prochains sauts, deux extrémités adjacentes en L2. Transport VXLAN (**VLAN 50**) : VTEP ↔ VTEP, destination de rien. Stockage (20/30/31) : baie ↔ hyperviseurs. **Gestion (10) : le poste de l'exploitant, un VPN, demain le lien inter-sites doivent l'atteindre** — elle reste dérivée et unique. Effet : sur six réseaux, **cinq** cessent d'exiger la moindre coordination entre deux hébergeurs, et « unique » redevient signifiant — seul ce qui doit l'être l'est | `underlay.yml`, `docs/hebergement/preparer-un-site-hebergeur.md` | P23 *(la borne de bande basse ne s'applique qu'aux destinations)* |
> **Ce que D-78 coûte, et ce qui la renverserait.** Le jour où l'un de ces liens devrait
> **franchir un site**, il redeviendrait une destination et exigerait une adresse unique.

View file

@ -74,7 +74,7 @@ objet fragile patiemment bricolé — c'est un artefact reproductible.
> Ce qui reste honnête à dire : la reconstruction prouve qu'un plan mène des machines nues à
> l'état voulu. Elle ne dit rien de la tenue d'un service **sous charge**, ni de sa mise à
> jour dans la durée — deux questions distinctes, et non traitées ici. Le degré de maturité,
> service par service, est tenu à jour dans `docs/catalogue-services.md`, et nous ne
> service par service, est tenu à jour dans `docs/projet/catalogue-services.md`, et nous ne
> présentons jamais comme « en production » un service que ce tableau ne donne pas pour tel.
---
@ -161,4 +161,4 @@ Trois principes résument la posture de Chezlepro :
---
*Détails techniques et runbooks : voir `AGENTS.md` (autorité du dépôt),
`docs/catalogue-services.md`, `docs/plan-et-generation.md`, `docs/positionnement.md`.*
`docs/projet/catalogue-services.md`, `docs/conception/plan-et-generation.md`, `docs/projet/positionnement.md`.*

View file

@ -116,7 +116,7 @@ couverts (§3), et les garder ici aurait fait adopter un outil pour un besoin d
**L'émancipation.** Le GUI écoute sur `127.0.0.1` avec un jeton par session : un modèle
**mono-utilisateur**, parfait tant que l'exploitant est une personne à son poste. Le jour où
un tenant est exploité par **son** organisation — c'est la trajectoire de
[`filiation-emancipation.md`](filiation-emancipation.md), et l'offre destinée aux OBNL y
[`filiation-emancipation.md`](../conception/filiation-emancipation.md), et l'offre destinée aux OBNL y
mène — il y a plusieurs humains, aux portées disjointes, sur des machines qui ne sont pas
les tiennes.
@ -148,3 +148,28 @@ autrement** — c'est exactement l'erreur que ce tableau portait.
> le périmètre sur lequel un ✅ a porté, les témoins du génome et lequel a décroché — ne
> franchit aucun seuil : rien de tout cela n'existe dans NetBox ou AWX, parce que rien de
> tout cela n'existe hors de ce modèle.
## 6. Licence et modèle
> Déplacé du `README.md` le 2026-10-15 : le README ne garde que le rôle du projet et le
> démarrage rapide. Le texte de la licence fait foi dans `LICENSE`.
### Pourquoi l'AGPLv3
Libre **et** copyleft fort. Chacun peut utiliser, exécuter, étudier et modifier Set-OPS —
**y compris commercialement**. En contrepartie, quiconque l'**offre comme service en réseau**
doit mettre à disposition ses modifications sous la même licence. Cela protège la **souveraineté**
(pas de captation propriétaire) **sans jamais interdire l'usage commercial** — cohérent avec le
principe fondateur : *tout est libre*.
### Modèle : libre + services
Set-OPS est **entièrement libre** ; aucune fonctionnalité n'est réservée ni verrouillée. Les
revenus, le cas échéant, proviennent de **services autour** du logiciel, jamais de sa fermeture :
- support / SLA, **hébergement géré**, formation, intégration, développement sur mesure ;
- **certification** d'écosystèmes (label *Souverain / Résilient / Exemplaire*).
Les artisans et coopératives peuvent exploiter Set-OPS pour **leur propre infrastructure sans
redevance** : l'usage interne ne déclenche aucune obligation de partage. Le copyleft AGPL ne vise
que la captation propriétaire (offrir Set-OPS en service fermé sans en partager les évolutions).

View file

@ -5,8 +5,8 @@
#
# Les 132 cibles documentees du Makefile disent chacune CE QU'ELLE FAIT. Aucune ne dit
# dans quel ORDRE, ni pourquoi maintenant, ni ce qu'il faut avoir mesure avant. Cette
# connaissance-la vivait en prose dans `docs/runbooks-exploitation.md`,
# `docs/implanter-un-tenant-sur-un-site.md` et `docs/preparer-un-site-hebergeur.md` — et
# connaissance-la vivait en prose dans `docs/exploitation/runbooks-exploitation.md`,
# `docs/hebergement/implanter-un-tenant-sur-un-site.md` et `docs/hebergement/preparer-un-site-hebergeur.md` — et
# la console offrait des boutons sans sequence.
#
# ON NE RECOPIE PAS LE LIBELLE D'UNE CIBLE. `scripts/runbooks.py` va le lire dans le
@ -84,6 +84,20 @@ variables:
DANS:
invite: "Jours avant le second temps"
facultatif: true
JEU:
invite: "Le jeu d'etat (openldap, postgresql, nextcloud... — voir restauration-etat)"
DEPUIS:
invite: "Reprendre a cette etape (vide = depuis le debut)"
valeurs: [sauvegarder, raser, creer, inseminer, armer, monter, parefeu, bilan]
facultatif: true
ARMER:
invite: "Armer sans marquer la pause"
valeurs: [oui]
facultatif: true
SOURCE:
invite: "Comparer a quel instantane (vide = celui d'avant rasage)"
valeurs: [candidat]
facultatif: true
VMID:
invite: "Identifiant Proxmox de la VM"
DIALECTE:
@ -100,7 +114,7 @@ runbooks:
- id: site-premier-jour
titre: "Le premier jour d'un site"
portee: site
doc: docs/runbooks-exploitation.md
doc: docs/exploitation/runbooks-exploitation.md
but: >-
Faire naitre un site hebergeur depuis une fabric nue : les machines, puis le moteur,
puis la forge qui permettra aux locataires de se reproduire. L'ordre n'est pas une
@ -170,7 +184,7 @@ runbooks:
- id: site-tenir
titre: "Tenir un site en etat"
portee: site
doc: docs/hebergeur-exploitation.md
doc: docs/hebergement/hebergeur-exploitation.md
but: >-
Les gestes reguliers de l'hebergeur : porter le genome a jour, appliquer un role aux
machines du site, et regarder ce que la fabric fait vraiment.
@ -199,6 +213,32 @@ runbooks:
- cible: site-intrants
nature: mesure
pourquoi: "Ce que ce site expose a ses locataires, derive et non declare deux fois."
- cible: fiches-site-deposer
nature: ecriture
pourquoi: >-
Deposer chez chaque locataire la fiche que le site lui destine. Son runner n'a pas
le depot du site : c'est par elle que son inventaire se genere sans lui. A commiter
dans le depot de chaque locataire.
- cible: forge-cles
nature: ecriture
fixes: {CONFIRMER: "true"}
pourquoi: >-
Enregistrer sur le depot de chaque locataire la cle de lecture que sa face publie, et
retirer celle qui ne l'est plus. Sans elle, son runner ne lit plus son depot prive.
- cible: forge-comptes
nature: ecriture
fixes: {CONFIRMER: "true"}
pourquoi: >-
Un compte par mainteneur declare, ouvert sur les seuls depots de ses locataires :
c'est par un ticket qu'il demande au site. Et le lecteur de la console du site, qui
met ces demandes dans « A traiter ».
- cible: forge-etancheite
nature: mesure
portee: poste
pourquoi: >-
Sans identifiant, chaque depot prive exige une authentification et le moteur
repond ; chaque runner de locataire lit son depot et pas celui d'un autre. Du
poste, qui voit le site et les runners.
- cible: site-verifier
nature: mesure
pourquoi: "Le playbook du site correspond-il encore aux couches declarees ?"
@ -208,6 +248,34 @@ runbooks:
- cible: routes-fabric-etat
nature: mesure
pourquoi: "Une route de zone manquante est invisible jusqu'au premier invite qui la traverse."
- cible: locataire-creer
nature: ecriture
variables: [TENANT, PARALLELE]
fixes: {CONFIRMER: "true"}
pourquoi: >-
Cloner les VM d'un locataire NOMME, d'apres la face reseau qu'il publie : le
runner du site materialise sans monter son depot. Memes machines, memes
parametres que `flotte-creer` sur l'instance montee (P93, P94).
# UNE SEULE MACHINE, A LA DEMANDE D'UN MAINTENEUR (2026-10-11). `locataire-creer`
# clone TOUTES les VM manquantes ; une demande de locataire en vise une. Les
# parametres viennent de la meme face reseau (`clonage`), par le meme chemin
# (`PARAMS_CLONAGE`). Sans `TENANT`, `creer-vm` exige une instance montee — que le
# site n'a pas : l'oubli se refuse, il ne cree rien de travers.
- cible: creer-vm
nature: ecriture
variables: [TENANT, HOTE]
duree: "~4 min 30"
pourquoi: >-
Cloner UNE machine d'un locataire nomme, d'apres sa face reseau publiee. Elle
n'existe pour le site que si son mainteneur l'a mise au plan, a applique
l'inventaire, puis a republie sa face.
- cible: locataire-raser
nature: destructif
variables: [TENANT, INSTANCE]
fixes: {CONFIRMER: "true"}
pourquoi: >-
Detruire les VM d'un locataire NOMME, d'apres sa face : les memes VMID que le
plan derive (P94). Son nom court s'ecrit en toutes lettres, comme pour `raser`.
- cible: site-raser
nature: destructif
variables: [SITE]
@ -220,7 +288,7 @@ runbooks:
- id: locataire-naitre
titre: "Faire naitre un ecosysteme de locataire"
portee: tenant
doc: docs/multi-instances.md
doc: docs/conception/multi-instances.md
but: >-
Du modele au plan monte : creer le dossier de l'ecosysteme, le rendre actif, puis
generer son inventaire. Tout l'adressage descend du seed `index` — on ne l'ecrit
@ -273,7 +341,7 @@ runbooks:
- id: locataire-materialiser
titre: "Preparer le terrain d'un locataire sur la fabric"
portee: site
doc: docs/implanter-un-tenant-sur-un-site.md
doc: docs/hebergement/implanter-un-tenant-sur-un-site.md
but: >-
Tout ce que l'HEBERGEUR pose avant qu'un locataire puisse exister : le placement, les
pools, le SDN, les pare-feux et la frontiere. Chaque devis se mesure avant de
@ -310,6 +378,12 @@ runbooks:
pourquoi: >-
Regenerer le registre des flux et les regles nftables depuis les `meta/flux.yml`
des roles. Tout ce qui suit en descend.
- cible: face-reseau-publier
nature: ecriture
pourquoi: >-
Publier ce que ce locataire demande a son site (`face-reseau.yml`) : ses machines,
ses zones, ses flux deja resolus. Le site ne lit plus que ce fichier. Apres `flux`,
et a commiter dans le depot du locataire.
- cible: devis-proxmox-fw
nature: mesure
pourquoi: "Le pare-feu est-ouest intra-locataire, derive du registre des flux."
@ -367,7 +441,7 @@ runbooks:
- id: locataire-deployer
titre: "Materialiser et deployer la flotte d'un locataire"
portee: tenant
doc: docs/vm-lifecycle.md
doc: docs/exploitation/vm-lifecycle.md
but: >-
Des VM au service rendu : creer les machines manquantes, deployer couche par couche,
puis mesurer. Rien n'est « pret » avant la recette.
@ -380,6 +454,14 @@ runbooks:
pourquoi: >-
Les VM manquantes, clonees depuis le gabarit dore. VMID, IP et VLAN sont DERIVES
du plan : on ne les saisit nulle part.
- cible: monter-flotte
nature: ecriture
duree: "long"
fixes: {CONFIRMER: "true"}
pourquoi: >-
Des VM qui viennent de naitre (ou de renaitre) a la flotte recettee : flux, AC et
DNS d'abord, tout le reste, puis `valider`. C'est la sequence du runner apres une
reconstruction ; chaque proprietaire d'etat y remet celui d'avant.
- cible: deployer-tout
nature: ecriture
duree: "long"
@ -414,7 +496,7 @@ runbooks:
- id: machine-une
titre: "Ajouter ou reprendre une seule machine"
portee: tenant
doc: docs/vm-lifecycle.md
doc: docs/exploitation/vm-lifecycle.md
but: >-
Le cycle d'UNE machine, du plan au service : la voir derivee, la creer, la deployer,
la verifier. Le meme chemin sert pour une machine neuve et pour une machine a
@ -453,11 +535,29 @@ runbooks:
- id: flotte-refaire
titre: "Raser et reconstruire un ecosysteme"
portee: tenant
doc: docs/vm-lifecycle.md
doc: docs/exploitation/vm-lifecycle.md
but: >-
La preuve la plus dure du moteur : detruire un ecosysteme et le refaire depuis le
code seul. A ne lancer que sur un chantier — la prod vit ailleurs.
etapes:
- cible: reconstruire-locataire
nature: destructif
portee: poste
duree: "long"
variables: [TENANT, DEPUIS, ARMER]
fixes: {CONFIRMER: "true"}
pourquoi: >-
TOUT, d'une commande, depuis le poste : sauvegarder, raser et recreer (runner du
site), amorcer, armer, monter (runner du locataire, etat remis), pare-feu, bilan.
Arret a la premiere etape en echec ; `DEPUIS` reprend. Les etapes ci-dessous sont
les memes, une a une.
- cible: sauvegarder-maintenant
nature: ecriture
pourquoi: >-
Deposer l'etat de chaque noeud JUSTE AVANT de raser. La reconstruction remet le
dernier instantane anterieur a la naissance des machines : sans ce depot, c'est
celui de la nuit, et la journee est perdue. Il est etiquete « avant-raser » et
garde jusqu'a la reconstruction suivante : c'est contre lui que jugent les temoins.
- cible: raser
nature: destructif
portee: poste
@ -472,17 +572,53 @@ runbooks:
duree: "long"
fixes: {CONFIRMER: "true"}
pourquoi: >-
Refaire tout depuis zero : les VM, puis le deploiement complet. Si le code ne
suffit pas, c'est ici qu'on l'apprend.
Refaire tout depuis zero : les VM, puis le deploiement complet — ou chaque role
proprietaire REMET l'etat de l'incarnation precedente (AC, annuaire, bases,
Nextcloud, courriel, DKIM). Si le code ne suffit pas, c'est ici qu'on l'apprend.
- cible: restauration-etat
nature: mesure
pourquoi: >-
Ce que chaque jeu est devenu : restaure (et depuis quel instantane), neuf, en
place. Un jeu EN ATTENTE bloque la sauvegarde de son noeud — c'est voulu.
- cible: temoins-etat
nature: mesure
variables: [HOTE, SOURCE]
pourquoi: >-
L'etat vivant est-il celui d'avant le rasage ? Une perte, ou une identite changee
(cles de l'AC, DKIM, instanceid de Nextcloud, mot de passe d'une entree de
l'annuaire), est un ecart. Le bilan dit ce que les roles ont fait ; les temoins
mesurent ce qui est revenu.
- cible: restauration-renoncer
nature: destructif
variables: [HOTE, JEU]
fixes: {CONFIRMER: "true"}
pourquoi: >-
Ecarter l'etat d'avant d'un jeu, sans le remettre. La sauvegarde reprend, et
l'ancien etat sortira de la retention : une decision, pas une reparation.
- cible: valider
nature: mesure
pourquoi: "Une reconstruction sans recette n'a rien prouve."
- cible: parefeu-verifier-flotte
nature: mesure
portee: poste
variables: [INSTANCE]
pourquoi: >-
La sonde `connectivite` est-elle saine sur chaque VM ? C'est le prealable : une VM
clonee nait sans ses options de pare-feu, et on n'active que sur une flotte saine.
- cible: parefeu-activer-flotte
nature: ecriture
portee: poste
variables: [INSTANCE]
fixes: {CONFIRMER: "true"}
pourquoi: >-
Tout activer d'un coup, puis lire la sonde `connectivite` que chaque VM rapporte a
la minute : seul ce qui change apres l'activation compte. Environ trois minutes.
# ─────────────────────────────────────────────────────────────────────────────
- id: gabarit
titre: "Le gabarit dore"
portee: site
doc: docs/procedure-template-debian13-proxmox.md
doc: docs/exploitation/procedure-template-debian13-proxmox.md
but: >-
La VM de reference que toute la flotte clone. Elle nait en q35/OVMF et ne se convertit
jamais : convertir depuis i440fx casse interfaces et disques.
@ -508,7 +644,7 @@ runbooks:
- id: mesurer
titre: "Mesurer sans rien ecrire"
portee: tenant
doc: docs/devis-services.md
doc: docs/exploitation/devis-services.md
but: >-
Les devis de service : ils confrontent ce que le plan derive a ce que le systeme rend
VRAIMENT, et sortent en erreur s'il y a un ecart. Une tache verte ne prouve pas qu'un
@ -594,7 +730,7 @@ runbooks:
- id: acces-admin
titre: "L'acces d'administration (tunnel WireGuard)"
portee: site
doc: docs/acces-administration.md
doc: docs/exploitation/acces-administration.md
but: >-
Un tunnel nominatif par locataire, declare par lui et borne a lui. Sans garde, un
ecosysteme s'ouvrirait un acces chez son voisin depuis son propre plan.
@ -611,7 +747,7 @@ runbooks:
- id: dns-public
titre: "Le DNS public et sa signature"
portee: tenant
doc: docs/dns-interne.md
doc: docs/conception/dns-interne.md
but: >-
Publier les zones du locataire, signees avant d'etre exposees. Les DS partent chez le
registraire : c'est le seul maillon que le moteur ne peut pas poser lui-meme.
@ -634,7 +770,7 @@ runbooks:
- id: filiation
titre: "Filiation, insemination, emancipation"
portee: tenant
doc: docs/filiation-emancipation.md
doc: docs/conception/filiation-emancipation.md
but: >-
D'ou vient cet ecosysteme, de quoi depend-il encore, et que faut-il couper pour qu'il
tienne seul. L'emancipation se mesure ; elle ne se decrete pas.
@ -688,7 +824,7 @@ runbooks:
- id: remise
titre: "La remise au client"
portee: poste
doc: docs/remise-au-client.md
doc: docs/hebergement/remise-au-client.md
but: >-
Deux temps qui ne se confondent pas : le paquet qu'on remet, puis le re-cle qui
mesure la revocation reelle. Tant que le temps 2 n'est pas fait, on detient encore
@ -721,7 +857,7 @@ runbooks:
- id: cles
titre: "Les cles hors du poste"
portee: poste
doc: docs/sortir-les-cles-du-poste.md
doc: docs/exploitation/sortir-les-cles-du-poste.md
but: >-
Ce qui n'existe QUE sur le poste meurt avec lui : les CLES, et les VOUTES qu'elles
ouvrent (gitignorees, donc sur aucune forge). Les deux sortent, sur un support qu'on
@ -764,7 +900,7 @@ runbooks:
- id: lire-le-plan
titre: "Lire le plan et ce qu'il derive"
portee: tenant
doc: docs/plan-et-generation.md
doc: docs/conception/plan-et-generation.md
but: >-
Tout ce qui se regarde sans rien changer : les registres du plan, leur validation, et
l'inventaire qui en descend. Le premier geste devant un ecosysteme qu'on ne connait pas.

152
docs/setup.md Normal file
View file

@ -0,0 +1,152 @@
# Installation et configuration
> **Pour qui :** l'exploitant qui monte son premier écosystème sur une grappe Proxmox. Le
> résumé est dans le [README](../README.md) ; ce document donne chaque étape, ses fichiers
> et ses pièges. Préparer le matériel d'un hébergeur est un autre parcours :
> [`hebergement/preparer-un-site-hebergeur.md`](hebergement/preparer-un-site-hebergeur.md).
## Prérequis
| Quoi | Détail |
|---|---|
| Grappe Proxmox | droits d'administration, API joignable depuis le poste |
| Jeton d'API Proxmox | utilisateur `ansible@pve` ou équivalent |
| Poste de pilotage | Linux, Python 3, Ansible, `make`, `git` ; `node` optionnel (garde JS du GUI) |
| Paire de clés SSH | la publique est injectée dans les VM par cloud-init |
| Phrase de passe de voûte | une par écosystème |
## 1. Cloner le moteur
```bash
git clone <url-de-Set-OPS> Set-OPS && cd Set-OPS
```
## 2. Créer l'instance
Une instance est un dépôt frère du moteur, créé depuis un modèle. Le dépôt public fournit
`socle` : PKI (step-ca), DNS interne (PowerDNS), edge TLS (nginx), magasin de courriel
(Dovecot). Les modèles assemblés par offre sont dans le dépôt privé `Set-OPS-modeles`
(`SETOPS_MODELES` les rend visibles).
```bash
make instance-modeles # modèles disponibles
make instance-creer NOM=OPS-MonOrg MODELE=socle # crée ../OPS-MonOrg ; INDEX=<n> fixe le seed
ln -s ../OPS-MonOrg instance # l'instance active (ou : make instance-utiliser NOM=OPS-MonOrg)
make instances # écosystèmes découverts, collisions d'index
```
`instance-creer` refuse un `index` déjà pris par un écosystème voisin : tout l'adressage en
dérive. Il ne bascule pas le lien `instance/` et ne fait pas `git init`.
## 3. Renseigner l'instance
`<inventaire>` est `principal` ou, à défaut, `production` (le nom que porte le modèle).
| Fichier | Valeur |
|---|---|
| `instance/inventories/<inventaire>/group_vars/all/10-intrants.yml` | `domaine_interne` (ex. `monorg.internal`) |
| `instance/plan/nomenclature.yml` | `index` (seed) et supernet |
| `instance/plan/domaines.yml` | domaine public, `autorite`, `edge` |
| `instance/plan/serveurs.yml` | placement Proxmox : nœud, stockage, disque, mémoire, cœurs |
Le GUI (`make inventaire-ui`) édite les mêmes registres. Valeurs communes :
[`conception/intrants-communs.md`](conception/intrants-communs.md).
## 4. Proxmox
```bash
make config # API, nœud, stockage, VMID et nom du gabarit
```
Chaque paramètre : [`exploitation/config-proxmox.md`](exploitation/config-proxmox.md).
Les identifiants Proxmox appartiennent à l'hébergeur : ils vivent dans **sa** voûte (celle
de l'underlay), pas dans celle du locataire.
## 5. Voûtes et clés
Une voûte par dépôt, chiffrée par `ansible-vault`, depuis le gabarit
[`exemples/vault.exemple.yml`](../exemples/vault.exemple.yml) :
`instance/inventories/<inventaire>/group_vars/all/vault.yml`.
Une clé par voûte, dans un fichier nommé d'après le dossier de l'écosystème, en minuscules :
```bash
install -m 600 /dev/null ~/.config/setops-vault-ops-monorg
$EDITOR ~/.config/setops-vault-ops-monorg # la phrase de passe, une ligne
python3 scripts/voutes.py etat # le moteur trouve-t-il chaque clé ?
```
Rien à exporter : le `Makefile` construit `ANSIBLE_VAULT_IDENTITY_LIST` avec
`scripts/voutes.py`. Créer une VM ouvre deux voûtes dans la même exécution (locataire et
hébergeur) : un seul `ANSIBLE_VAULT_PASSWORD_FILE` ne suffit pas.
Les clés n'existent que sur le poste. Les sortir sur un support chiffré :
[`exploitation/sortir-les-cles-du-poste.md`](exploitation/sortir-les-cles-du-poste.md).
## 6. Gabarit Debian 13 (une fois par grappe)
Créer une VM Debian 13 minimale, joignable par SSH avec clé, puis :
```bash
make preparer-modele
make verifier-modele
make nettoyer-modele CONFIRMER=true
```
Convertir la VM en gabarit Proxmox sous le nom enregistré par `make config`
(`proxmox_clone_source_nom`, `modeleSetOPS` par défaut). Un autre nom fait échouer le
clonage. Machine **q35/OVMF** : ne jamais convertir depuis i440fx.
Procédure : [`exploitation/procedure-template-debian13-proxmox.md`](exploitation/procedure-template-debian13-proxmox.md) ;
contenu justifié : [`exploitation/gabarit-debian13-justification.md`](exploitation/gabarit-debian13-justification.md).
## 7. Générer l'inventaire
```bash
make instancier # ce que le plan produit, en diff
make instancier-appliquer FORCE=1 # première génération seulement
make serveurs # VMID / IP dérivés
```
Les hôtes naissent en `etat: planifie`.
## 8. Créer et déployer
```bash
make creer-vm HOTE=infra-dns-01 # clone + cloud-init, attend que la VM réponde
# passer l'hôte en etat: actif dans instance/plan/serveurs.yml, puis :
make instancier-appliquer
make deployer HOTE=infra-dns-01 # couche par couche, dans l'ordre du graphe
```
Toute la flotte : `make flotte-creer CONFIRMER=true` puis `make deployer-tout CONFIRMER=true`,
ou `make reconstruire CONFIRMER=true` (créer puis déployer). Les déploiements ne visent que
les hôtes actifs.
## 9. Vérifier
```bash
make inventaire-verifier # registres, inventaire, gardes
make verifier # lint, tests, inventaire, ordre, flux, syntaxe, puis les preuves
make valider # recette sur la flotte déployée
```
## Configuration de référence
| Élément | Rôle | Défaut |
|---|---|---|
| `instance/` | lien vers le dépôt du locataire actif | — |
| `underlay.yml` | lien vers l'underlay du site actif | `underlay.yml` à la racine |
| `ansible.cfg` | configuration Ansible du moteur | dans le dépôt |
| `.ansible/` | répertoires temporaires d'Ansible | créé par `make` |
Variables d'environnement et de `make` : [`usage.md`](usage.md).
## Pannes connues au premier montage
| Symptôme | Cause | Remède |
|---|---|---|
| `Attempting to decrypt but no vault secrets found` | clé de voûte absente ou mal nommée | étape 5, `python3 scripts/voutes.py etat` |
| `make instancier-appliquer` refuse d'écrire | diff non vide, première génération | `FORCE=1` la première fois seulement |
| le clonage ne trouve pas sa source | nom du gabarit ≠ `proxmox_clone_source_nom` | renommer le gabarit ou refaire `make config` |
| une sortie d'Ansible vide, sans erreur | Ansible exige un tube sur sa sortie | `set -o pipefail; make … < /dev/null 2>&1 \| cat` |

315
docs/usage.md Normal file
View file

@ -0,0 +1,315 @@
# Utilisation
> **Pour qui :** l'exploitant et le mainteneur — la référence de l'outil en ligne de
> commande. Set-OPS est un outil CLI : tout passe par `make`, qui appelle Ansible et les
> scripts de `scripts/`. Installation : [`setup.md`](setup.md).
## Conventions
| Convention | Sens |
|---|---|
| `make <sujet>-plan` | compare le réel à ce que le plan dérive ; **n'écrit rien** |
| `make <sujet>-appliquer CONFIRMER=true` | réconcilie ; sans `CONFIRMER=true`, refuse |
| `make <sujet>-verifier` | contrôle ; n'écrit rien (la plupart lisent le dépôt seul) |
| `make <sujet>-etat`, `-mesurer`, `-recenser` | regarde le réel ; ne corrige rien |
| `DESTRUCTIF` dans l'aide | exige `CONFIRMER=true` **et** le nom de la cible (`INSTANCE=`, `SITE=`) |
| `FORCE=1` | passe outre le diff de `instancier-appliquer` ; première génération seulement |
| `MODE_CHECK=1` | `deployer-tout` en essai à blanc (`--check`) |
| `ARGS="…"` | arguments passés tels quels au script ou au playbook (ex. `ARGS=--check`) |
`make` seul (ou `make aide`) affiche l'aide détaillée. L'aide de chaque cible (`## …` dans
le `Makefile`) est la source des tableaux ci-dessous ; une preuve exige que chaque cible
publique y figure.
**Sortie d'Ansible.** Ansible exige une sortie bloquante : depuis un shell non interactif,
toujours `set -o pipefail; make … < /dev/null 2>&1 | cat`. Sans tube, l'échec est silencieux.
## Variables de `make`
| Variable | Sens | Cibles |
|---|---|---|
| `HOTE` | une VM, par son nom court | `deployer`, `creer-vm`, `hote-afficher`, … |
| `GROUPE` | un groupe Ansible (= un playbook de `playbooks/groupes/`) | `deployer-groupe`, `appliquer`, `site-appliquer` |
| `TENANT` | le dépôt d'un locataire (`OPS-X`) vu depuis le site | `locataire-creer`, `inseminer`, `placement-plan`, … |
| `INSTANCE` | le nom court d'un écosystème, exigé par les cibles destructives | `raser`, `locataire-raser`, `parefeu-*-flotte` |
| `SITE` | le dépôt d'un site, exigé par `site-raser` | `site-raser` |
| `CONFIRMER` | `true` pour écrire | toute cible `-appliquer` ou destructive |
| `LIMITE` | motif d'hôtes Ansible | `faits`, `verifier-hote` |
| `VERS` | un répertoire de destination (support externe) | `cles-exporter`, `voutes-exporter`, `remise-paquet`, … |
| `DEPOT` | un seul dépôt du génome | `publier`, `genome-pousser` |
| `PARALLELE` | nombre de VM créées à la fois | `flotte-creer`, `locataire-creer` |
| `NOM`, `MODELE`, `INDEX` | nouvel écosystème | `instance-creer`, `instance-utiliser`, `model-creer` |
| `DIALECTE` | CLI du commutateur (`cisco`, `binardat`) | `devis-reseau` |
## Variables d'environnement
| Variable | Sens | Défaut |
|---|---|---|
| `SETOPS_INSTANCE` | dépôt du locataire visé | `instance` (le lien) |
| `SETOPS_UNDERLAY` | underlay du site visé | `underlay.yml` (le lien) |
| `SETOPS_INVENTAIRE` | inventaire de l'instance ; exporté par le `Makefile` | `principal`, puis `production` |
| `ANSIBLE_VAULT_IDENTITY_LIST` | les clés de toutes les voûtes ; construit par `scripts/voutes.py` | dérivé de `~/.config/setops-vault-*` |
| `SETOPS_PROXMOX_API` | `hote:port` de l'API Proxmox le temps d'une commande (par un rebond) | l'adresse de la carte |
| `SETOPS_SSH_KEY` | clé SSH vers les nœuds Proxmox | l'agent SSH |
| `SETOPS_MODELES` | dossiers de modèles en plus de `exemples/modeles/` | — |
| `SETOPS_DIALECTE` | CLI du commutateur pour `devis-reseau` | `dialecte` de l'underlay |
| `SETOPS_FORGE_URL` | adresse de la forge pour `forge_cles.py` | — |
| `SETOPS_FORGE_MDP`, `FORGEJO_ADMIN` | mot de passe d'administration de la forge, depuis la voûte, le temps d'une commande | — |
Viser un autre site ou un autre locataire sans toucher aux liens :
```bash
SETOPS_UNDERLAY=../SITE-Technolibre/underlay.yml make frontiere-plan
SETOPS_INSTANCE=../OPS-Chezlepro make instancier
```
## Flux de travail courants
```bash
# Modifier l'écosystème
$EDITOR instance/plan/applications.yml
make instancier && make instancier-appliquer && make deployer HOTE=<vm>
# Contrôler une couche d'infrastructure, puis la réconcilier
make frontiere-plan
make frontiere-appliquer CONFIRMER=true
# Publier le moteur et les plans vers les forges
make publier
make wiki-publier WIKI_REMOTE=<url>.wiki.git
```
## Scripts appelés directement
Les cibles `make` couvrent l'exploitation. Quelques scripts s'utilisent aussi seuls ; chacun
documente ses options (`--help`) dans sa docstring.
| Script | Usage |
|---|---|
| `scripts/voutes.py etat` | les voûtes et leurs clés, vues par le moteur |
| `scripts/contexte.py` | le contexte actif (site, locataire) et ce que dit chaque indice |
| `scripts/orchestrer.py ordre` | l'ordre de déploiement des groupes |
| `scripts/dnssec.py generer <zone>` | crée la clé DNSSEC d'une zone, dans la voûte du locataire |
| `scripts/vpn_admin.py cle-appareil`, `config` | un appareil dans le tunnel des administrateurs |
| `scripts/dns_bascule.py --serveur <ip>` | le devis de bascule DNS contre un serveur précis |
## Commandes
### Vérifier et prouver
| Commande | Effet |
|---|---|
| `make aide` | Affiche l'aide detaillee du moteur (au-dela de cette liste) |
| `make verifier` | Rejoue les preuves SANS reecrire le rapport (verification rapide) |
| `make prouver` | Execute les preuves et ecrit docs/audit/preuve-<date>.md |
| `make test` | Lance les tests unitaires (derivation de nomenclature et d'inventaire) |
| `make lint` | Passe ansible-lint sur tout le depot |
| `make syntaxe` | Verifie la syntaxe de TOUS les playbooks (modele, hote, groupes, proxmox) |
| `make syntaxe-modele` | Verifie la syntaxe du playbook de preparation du gabarit dore |
| `make syntaxe-verification-modele` | Verifie la syntaxe du playbook de verification du gabarit |
| `make syntaxe-nettoyage` | Verifie la syntaxe du playbook de nettoyage du gabarit |
| `make syntaxe-verification-hote` | Verifie la syntaxe du playbook de verification d'hote |
| `make syntaxe-groupes` | Verifie la syntaxe de TOUS les playbooks de groupe (playbooks/groupes/*.yml) |
| `make syntaxe-proxmox` | Verifie la syntaxe du playbook de clonage de VM |
| `make ci` | Verifie le depot comme la CI : modele public monte a l'ecart, sans toucher ton instance |
| `make inventaire-verifier` | Verifie que l'inventaire se parse (voute dechiffree) |
| `make serveurs-verifier` | Valide le registre des serveurs |
| `make bases-verifier` | Valide le registre des bases de donnees |
| `make domaines-verifier` | Valide le registre des domaines |
| `make applications-verifier` | Valide le registre des applications |
| `make ports-verifier` | Deux roles co-localises revendiquent-ils le meme port ? (lecture seule) |
| `make intrants-verifier` | Tout intrant qu'un role EXIGE est-il fourni par l'instance ? (lecture seule) |
| `make site-verifier` | Verifie que playbooks/site.yml correspond aux couches declarees |
| `make flux-verifier` | Verifie que le registre des flux correspond aux meta/flux.yml des roles |
| `make schema` | Regenere docs/audit/schema-plan.json depuis les registres et les validateurs |
| `make plan-recette` | Regenere docs/audit/plan-de-recette.md depuis le wiki |
| `make sonder` | Sonde une cible et DIT ce qui distingue — CIBLE=<hote\|ip> [PORTS="22 443"] |
| `make valider` | Passe la recette de validation sur la flotte |
### Instances et modèles
| Commande | Effet |
|---|---|
| `make instance-modeles` | Liste les modeles d'ecosysteme disponibles |
| `make instance-creer` | Cree un nouvel ecosysteme depuis un modele — NOM=<nom> MODELE=<modele> |
| `make instance-utiliser` | Bascule l'instance active (symlink instance/) vers un dossier frere — NOM=<dossier> |
| `make instance-courante` | Affiche vers quel ecosysteme pointe l'instance active |
| `make instances` | Liste les ecosystemes decouverts (dossiers freres) et signale les collisions d'index |
| `make model-creer` | Cree un modele d'ecosysteme — MODE=<mode> NOM=<nom> |
| `make genome` | Les depots requis a la reproduction de cet ecosysteme, et leur etat |
| `make genome-inscrire` | Inscrit la parente dans l'instance (instance/parente.yml) |
| `make genome-verifier` | La parente inscrite tient-elle encore ? (code de sortie) |
### Plan et inventaire
| Commande | Effet |
|---|---|
| `make config` | Affiche la configuration Proxmox lue par le moteur |
| `make inventaire-ui` | Ouvre la console d'exploitation (GUI web) sur l'inventaire actif |
| `make instancier` | Genere hosts.yml depuis le plan (sans l'appliquer) |
| `make instancier-appliquer` | Applique l'inventaire genere — FORCE=1 pour passer outre le diff |
| `make serveurs` | Liste les serveurs declares au plan |
| `make serveurs-bootstrap` | REPRISE : (re)constitue plan/serveurs.yml depuis un inventaire existant |
| `make bases` | Liste les bases de donnees declarees au plan |
| `make domaines` | Liste les domaines declares au plan |
| `make applications` | Liste les applications declarees au plan |
| `make applications-bootstrap` | Amorce les applications declarees au plan |
| `make hote-afficher` | Affiche tout ce que le plan derive pour un hote — HOTE=<nom> |
| `make inventaire` | Verifie l'inventaire et en affiche le graphe (lab puis production) |
| `make inventaire-lister` | Affiche l'inventaire complet (JSON) |
| `make inventaire-graphe` | Affiche le graphe des groupes de l'inventaire |
| `make inventaire-hote` | Affiche les variables derivees d'un hote — HOTE=<nom> |
| `make inventaire-lab` | Affiche le graphe de l'inventaire de laboratoire |
| `make inventaire-production` | Affiche le graphe de l'inventaire de production |
### Gabarit et VM
| Commande | Effet |
|---|---|
| `make preparer-modele` | Prepare le gabarit dore (VM de reference clonee pour chaque hote) |
| `make verifier-modele` | Verifie le gabarit dore |
| `make nettoyer-modele` | Nettoie le gabarit avant capture — exige CONFIRMER=true |
| `make cloner-vm` | Clone une VM depuis le gabarit dore — HOTE=<nom> VMID=<id> |
| `make creer-vm` | Cree une VM et attend qu'elle soit joignable — HOTE=<nom> [TENANT=<depot>] |
| `make flotte-creer` | Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n |
| `make raser` | DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom> [TENANT=<depot>] |
| `make reconstruire` | Reconstruit un ecosysteme depuis zero : VM puis deploiement complet |
| `make myDay` | Alias strict de `reconstruire` (meme cible, memes gardes) |
| `make monter-flotte` | Monte une flotte deja creee : flux, AC et DNS, tout le reste, recette — CONFIRMER=true |
| `make reconstruire-locataire` | Rase et reconstruit un locataire, du site a la recette, etat remis — TENANT=OPS-X CONFIRMER=true |
### Déployer
| Commande | Effet |
|---|---|
| `make ansible-runtime` | Prepare le repertoire temporaire local d'Ansible (prerequis interne des cibles qui deploient) |
| `make site` | Regenere playbooks/site.yml depuis les couches et le graphe de dependances |
| `make appliquer` | Applique un groupe a la flotte — GROUPE=<groupe> |
| `make deployer` | Deploie un hote, couche par couche, dans l'ordre du graphe — HOTE=<nom> |
| `make deployer-groupe` | Deploie un seul groupe sur toute la flotte — GROUPE=<groupe> |
| `make deployer-tout` | Deploie TOUTE la flotte dans l'ordre des couches |
| `make verifier-deploiement` | Verifie l'etat de la flotte apres deploiement |
| `make verifier-hote` | Passe le playbook de verification sur un hote |
| `make faits` | Interroge les faits Ansible de la flotte — LIMITE=<motif> |
| `make cacher-paquets` | Tire les paquets NON DEBIAN dans le cache du controleur (a faire en ligne) |
### Réseau : flux, frontière, SDN, pare-feu
| Commande | Effet |
|---|---|
| `make flux` | Regenere le registre des flux et les regles nftables depuis les meta/flux.yml |
| `make devis-reseau` | Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures. DIALECTE=cisco\|binardat |
| `make devis-opnsense` | Devis OPNsense (frontiere nord/sud), derive du registre des flux (pair: externe) |
| `make devis-opnsense-verifier` | Verifie le devis de la frontiere nord/sud (aucune ecriture) |
| `make frontiere-plan` | Ecart entre la frontiere OPNsense et son devis (aucune ecriture) |
| `make frontiere-appliquer` | Reconcilie la frontiere : cree ce qui manque, RETIRE ce qui est perime. CONFIRMER=true |
| `make frontiere-mesurer` | La frontiere refuse-t-elle ce qui n'est pas declare ? (sonde reelle, aucune ecriture) |
| `make devis-proxmox-fw` | Devis pare-feu Proxmox (est-ouest intra-tenant), derive du registre des flux |
| `make devis-proxmox-fw-verifier` | Verifie le devis du pare-feu est-ouest Proxmox (aucune ecriture) |
| `make proxmox-fw-plan` | Ecart entre le pare-feu est-ouest Proxmox et son devis (aucune ecriture) — ARGS="--objets-seulement" ou "--vm <vmid>" |
| `make proxmox-fw-appliquer` | Reconcilie le pare-feu est-ouest : IPSets, groupes, affectations. CONFIRMER=true |
| `make proxmox-fw-eprouver` | Verifie qu'activer le pare-feu d'UNE VM ne coupe rien (aucune ecriture) — TENANT=<OPS-X> HOTE=<vm> |
| `make proxmox-fw-activer-vm` | Active le pare-feu d'UNE VM en le prouvant avant/apres — TENANT= HOTE= CONFIRMER=true |
| `make parefeu-verifier-flotte` | La sonde `connectivite` est-elle saine sur chaque VM (prealable a l'activation) — INSTANCE=OPS-X |
| `make parefeu-activer-flotte` | Active le pare-feu Proxmox de toutes les VM, juge par la sonde `connectivite` — INSTANCE=OPS-X CONFIRMER=true |
| `make devis-sdn` | Devis SDN EVPN (zone + VNets + sous-reseaux par tenant), derive du seed |
| `make devis-sdn-verifier` | Verifie le devis SDN EVPN — zones, VNets, sous-reseaux (aucune ecriture) |
| `make sdn-plan` | Ecart entre le SDN EVPN (+ sortie des VRF) et son devis (aucune ecriture) |
| `make sdn-appliquer` | Reconcilie le SDN : cree ce qui manque, RETIRE ce qui est perime. CONFIRMER=true |
| `make devis-proxmox-pools` | Devis des pools Proxmox (un par tenant), derive du plan |
| `make devis-proxmox-pools-verifier` | Verifie le devis des pools Proxmox (aucune ecriture) |
| `make placement-plan` | Le noeud, stockage, pont et gabarit du tenant existent-ils sur ce cluster ? (aucune ecriture) [TENANT=<depot>] |
| `make underlay` | Underlay (fabric physique cluster-global : mgmt/iSCSI/Ceph) : affiche + valide (P23) |
| `make underlay-plan` | Confronte l'underlay DECLARE au reel (API du cluster + sonde TCP) — n'ecrit rien |
| `make mtu-mesurer` | L'invite porte-t-il le MTU de sa zone SDN ? (aucune ecriture) |
| `make routes-fabric-etat` | Les hyperviseurs routent-ils toutes les zones du SITE ? — ne corrige rien |
| `make vpn-admin-plan` | Acces WireGuard des admins : ce que la frontiere porte face au plan (aucune ecriture) |
| `make vpn-admin-appliquer` | Pose l'instance et les pairs declares, retire ceux qui ne le sont plus — CONFIRMER=true |
### Devis de service
| Commande | Effet |
|---|---|
| `make identite-plan` | Ecart entre l'identite deployee et ce que le plan derive (aucune ecriture) |
| `make certificats-plan` | Ecart entre les certificats sur disque et ceux reellement servis (aucune ecriture) |
| `make expositions-plan` | Chaque exposition du plan repond-elle ? (edge et poste, aucune ecriture) |
| `make expositions-etat` | Chaque exposition est-elle servie sous un certificat qui la porte ? — ne corrige rien |
| `make postgresql-plan` | Chiffrement impose et portee des acces PostgreSQL (aucune ecriture) |
| `make courriel-plan` | Chaine Postfix -> LDAP -> Dovecot -> IMAP (aucune ecriture) |
| `make versions-mesurer` | De combien nos epinglages ont-ils vieilli ? (interroge les amonts, aucune ecriture) |
### PKI et DNS
| Commande | Effet |
|---|---|
| `make ca-racine` | Recupere la racine de l'AC interne + son empreinte (a verifier AVANT de l'installer) |
| `make ca-empreinte` | Empreinte de la racine, lue SUR l'AC (le temoin de comparaison) |
| `make dnssec-ds` | DS a remettre au registraire, calcules depuis la voute du locataire (aucune ecriture) |
| `make dnssec-verifier` | Cle en voute pour chaque zone signee, et DS du registre conforme ? (aucune ecriture) |
| `make dns-bascule-devis` | Basculer nos serveurs de noms changerait-il quelque chose ? Plan contre DNS en service (aucune ecriture) |
### Site et locataires
| Commande | Effet |
|---|---|
| `make site-intrants` | Les intrants que le SITE monte expose a ses locataires (derives) |
| `make site-intrants-verifier` | Le locataire monte suit-il encore son site ? (aucune ecriture) |
| `make face-reseau-publier` | Chaque locataire publie sa face reseau pour son site (a commiter chez lui, apres `make flux`) |
| `make fiches-site-deposer` | Le site depose chez chacun de ses locataires la fiche qu'il lui destine (a commiter chez eux) |
| `make site-decrire` | Les machines de l'hebergeur declarees dans l'underlay |
| `make site-inventaire` | L'inventaire dynamique du site, tel qu'Ansible le voit |
| `make site-appliquer` | Applique un role aux machines du site — GROUPE=<serveur_ops_site\|...> |
| `make site-deployer-tout` | Deploie TOUT le site dans l'ordre des couches — CONFIRMER=true |
| `make site-creer` | Cree les machines du site depuis l'underlay — CONFIRMER=true |
| `make site-raser` | DESTRUCTIF : detruit les VM du SITE — CONFIRMER=true ET SITE=<depot> |
| `make locataire-creer` | Le SITE cree les VM d'un locataire d'apres sa face reseau — TENANT=<depot> CONFIRMER=true [PARALLELE=n] |
| `make locataire-raser` | DESTRUCTIF : le SITE rase les VM d'un locataire d'apres sa face reseau — TENANT=<depot> CONFIRMER=true INSTANCE=<nom court> [HOTE=h] |
| `make gabarit-etat` | Le gabarit porte-t-il ce que le SITE declare ? — ne corrige rien, regarde |
| `make inseminer` | Le SITE amorce le runner d'un tenant — TENANT=<dossier> [HOTE=<nom>] |
| `make emancipation-prouver` | Prouve qu un lien est coupe — SERVICE=<artefacts\|genome\|resolveur> [HOTE=] CONFIRMER=true |
### Génome, forge, publication
| Commande | Effet |
|---|---|
| `make publier` | Publie le genome d'un geste : eregion PUIS la forge du site, et le verifie — DEPOT=<nom> pour un seul |
| `make genome-pousser` | Le runner du site pousse le genome sur la forge du site — DEPOT=<nom> pour un seul |
| `make genome-etat` | Ce que la forge du site porte, face au poste — ne corrige rien, regarde |
| `make forge-amorcer` | Amorce la forge d'un SITE neuf avec le genome — CONFIRMER=true |
| `make forge-cles` | Le site enregistre sur le depot de chaque locataire la cle de lecture que sa face publie — CONFIRMER=true pour ecrire |
| `make forge-comptes` | Les comptes de la forge du site : un par mainteneur (plan du site), et le lecteur de la console — CONFIRMER=true pour ecrire |
| `make forge-etancheite` | La forge du site laisse-t-elle chaque runner lire ses depots, et personne d'autre ? (mesure, aucune ecriture) |
| `make depots-perimes` | Depots qu'un runner ne declare plus : regarde, et retire avec CONFIRMER=true |
| `make wiki-publier` | Publie le wiki (wiki/) vers la forge |
| `make fiches` | Regenere la fiche de chaque role (docs/roles/) — ROLE=<nom> pour une seule |
### Sauvegardes et état
| Commande | Effet |
|---|---|
| `make sauvegarder-maintenant` | Depose l'etat de chaque noeud TOUT DE SUITE (avant de raser) — HOTE=<hote> optionnel |
| `make restauration-etat` | Ce qui attend dans le depot, et ce que chaque jeu est devenu — HOTE=<hote> optionnel |
| `make temoins-etat` | L'etat vivant est-il celui d'avant le rasage ? (lecture seule) — HOTE= SOURCE=candidat optionnels |
| `make restauration-renoncer` | Ecarte l'etat anterieur d'un jeu, sans le remettre — HOTE= JEU= CONFIRMER=true |
| `make depot-hors-site` | Copie le depot de sauvegarde du site HORS du site — VERS=<repertoire> |
### Clés et voûtes
| Commande | Effet |
|---|---|
| `make cles-recenser` | Montre ce qui n'existe QUE sur ce poste (sans rien ecrire) |
| `make voutes-recenser` | Montre les voutes de ce poste (gitignorees : sur aucune forge), sans rien ecrire |
| `make voutes-exporter` | Sort les voutes (deja chiffrees) vers un support, relues — VERS=<repertoire> NOM=<base> |
| `make cles-exporter` | Sort les cles du poste, chiffrees et relues — VERS=<repertoire> |
| `make cles-compagnons` | Depose le script de restauration sur une cle DEJA faite — VERS=<repertoire> |
| `make cles-restaurer` | Remet les cles en place depuis une archive — ARCHIVE=<fichier> |
### Remise au client
| Commande | Effet |
|---|---|
| `make remise-recenser` | Ce qu'une remise au client emporterait, sans rien ecrire |
| `make remise-paquet` | Temps 1 : fabrique le paquet de remise, chiffre et relu — VERS=<repertoire> |
| `make remise-inscrire` | Inscrit la remise chez le locataire — RECU_PAR="…" COURRIEL="…" [DANS=30] |
| `make remise-verifier` | Etat de la remise : temps 1 fait ? temps 2 du, ou echu ? |
| `make remise-recleer` | Temps 2 : mesure la revocation reelle, puis estampille — CONFIRMER=true |

View file

@ -5,7 +5,7 @@
# generateur) est generique ; c'est ici qu'on dit "qui on est". A placer dans
# inventories/<env>/group_vars/all.yml de l'instance.
#
# Voir docs/plan-et-generation.md et docs/positionnement.md.
# Voir docs/conception/plan-et-generation.md et docs/projet/positionnement.md.
# Domaine DNS interne de l'instance. Les roles en derivent zones, FQDN, base DN
# LDAP, expediteurs courriel, etc. (ex. dc=acme,dc=local pour acme.local).

View file

@ -32,7 +32,8 @@ vault_postgresql_keycloak: ""
vault_bd_keycloak: ""
vault_bd_forgejo: ""
vault_bd_icingadb: ""
# La base des COMPTES de la console (mode `db`) — distincte de celle du moteur.
# La base de la console (vigie) dans tous les modes : comptes en mode `db`, préférences
# et migrations partout — distincte de celle du moteur.
vault_bd_icingaweb2: ""
# --- Forge (Forgejo) ---

View file

@ -22,7 +22,7 @@ depots:
composant: main
# Sur CHAQUE machine, via `client_pki`. Le paquet le plus critique de la liste.
paquets:
step-cli: 0.30.6-1
step-cli: 0.31.0-1
icinga:
uri: https://packages.icinga.com/debian
@ -37,12 +37,12 @@ depots:
icinga-archive-keyring: 2.0.0-1+debian13
icingacli: 2.14.0-2+debian13
icingadb: 1.5.1-8+debian13
icingadb-redis: 8.2.9-1+debian13
icingadb-redis: 8.2.10-1+debian13
icingadb-web: 1.4.0-1+debian13
icinga-l10n: 1.4.0-1+debian13
icinga-php-legacy: 1.1.0-1+debian13
icinga-php-library: 1.0.0-2+debian13
icinga-php-thirdparty: 1.0.0-2+debian13
icinga-php-library: 1.0.1-1+debian13
icinga-php-thirdparty: 1.0.1-1+debian13
icingaweb2: 2.14.0-2+debian13
icingaweb2-common: 2.14.0-2+debian13
icingaweb2-module-monitoring: 2.12.6-1+debian13
@ -53,7 +53,12 @@ depots:
suite: stable
composant: main
# `alloy` est universel comme `step-cli` : `client_metrique` le pose partout.
# RELEVES LE 2026-10-03 sur ce que portent les locataires depuis leur reconstruction du
# 2026-10-01 : leurs runners n'ont pas ce cache et avaient tire la derniere version
# publiee. Le cache du poste, lui, servait encore les anciennes — un deploiement depuis
# le poste aurait tente de les RETROGRADER (`paquets_tiers` installe les .deb du cache).
# Le site, encore en 1.19.2 / 13.2.1 / 3.7.7, montera a son prochain passage.
paquets:
alloy: 1.19.2-1
grafana: 13.2.1
loki: 3.7.7
alloy: 1.20.1-1
grafana: 13.2.3
loki: 3.7.8

View file

@ -0,0 +1,121 @@
---
# L'ETAT D'UNE INCARNATION PRECEDENTE — voir roles/client_backup/templates/restaurer.sh.j2
#
# make restauration-etat [HOTE=...]
# ce qui attend dans le depot, et ce que chaque jeu est devenu (lecture seule) ;
# make sauvegarder-maintenant [HOTE=...]
# deposer TOUT DE SUITE, et attendre que ce soit fait. A faire juste avant `raser` :
# sinon la reconstruction remet l'etat de la derniere nuit, et perd la journee ;
# make temoins-etat [HOTE=...] [SOURCE=candidat]
# l'etat VIVANT est-il celui d'avant le rasage ? Compare a l'instantane etiquete
# « avant-raser » (lecture seule). Une perte, ou une identite changee (cles de l'AC,
# DKIM, instanceid, mot de passe d'une entree de l'annuaire), fait echouer le jeu ;
# make restauration-renoncer HOTE=<hote> JEU=<jeu> CONFIRMER=true
# ECARTER un etat anterieur sans le remettre. La sauvegarde du noeud, qui refusait
# de deposer pour ne pas le chasser de la retention, reprend — et l'etat d'avant
# finira par sortir de la retention. C'est une decision, pas une reparation.
#
# La REMISE, elle, n'a pas de cible : elle est faite par le role proprietaire de l'etat,
# au deploiement, au moment ou il le creerait neuf.
- name: L'etat d'une incarnation precedente
hosts: client_backup
become: true
gather_facts: false
vars:
restauration_action: etat
restauration_jeu: ""
restauration_source: avant-raser
tasks:
- name: Refuser un renoncement sans jeu nomme
ansible.builtin.assert:
that:
- restauration_jeu | length > 0
fail_msg: "JEU=<jeu> requis (voir `make restauration-etat` pour les noms)."
when: restauration_action == 'renoncer'
- name: Lire ce qui attend, et ce qui a ete tranche
ansible.builtin.command:
argv: [/usr/local/sbin/setops-restaurer, etat]
register: restauration_lue
changed_when: false
failed_when: false
when: restauration_action == 'etat'
- name: Etat
ansible.builtin.debug:
msg: "{{ (restauration_lue.stdout_lines | default([])) + (restauration_lue.stderr_lines | default([])) }}"
when: restauration_action == 'etat'
# L'unite est `oneshot` : `systemctl start` rend la main quand le depot est fait, et
# echoue s'il a echoue — y compris quand la garde refuse (etat d'avant non remis).
- name: Ce noeud a-t-il quelque chose a deposer ?
ansible.builtin.stat:
path: /etc/systemd/system/setops-sauvegarde.service
register: restauration_unite
when: restauration_action == 'sauvegarder'
# LE DEPOT D'AVANT RASAGE EST ETIQUETE (2026-10-07), et la retention le garde jusqu'a la
# reconstruction suivante. Un noeud qui n'a pas encore cette unite deposerait SANS
# etiquette, et la premiere sauvegarde d'apres reconstruction le chasserait : c'est
# exactement ce qui s'est passe a M4. On refuse plutot que de deposer a moitie.
- name: L'unite du depot d'avant rasage est-elle deployee ?
ansible.builtin.stat:
path: /etc/systemd/system/setops-sauvegarde-avant-raser.service
register: restauration_unite_avant_raser
when:
- restauration_action == 'sauvegarder'
- restauration_unite.stat.exists
- name: Refuser un depot que la retention ne garderait pas
ansible.builtin.assert:
that:
- restauration_unite_avant_raser.stat.exists
fail_msg: >-
setops-sauvegarde-avant-raser.service absente : redeployer client_backup
(make appliquer GROUPE=client_backup) avant de raser.
when:
- restauration_action == 'sauvegarder'
- restauration_unite.stat.exists
- name: Deposer maintenant, etiquete avant rasage
ansible.builtin.systemd:
name: setops-sauvegarde-avant-raser.service
state: started
when:
- restauration_action == 'sauvegarder'
- restauration_unite.stat.exists
# Code 4 = rien a comparer (aucun etat, ou aucun instantane d'avant rasage) : ce n'est
# pas un echec. Code 1 = ecart : on affiche d'abord, on echoue ensuite.
- name: Temoins — l'etat vivant contre l'instantane d'avant rasage
ansible.builtin.command:
argv: >-
{{ ['/usr/local/sbin/setops-restaurer', 'temoins']
+ (['--candidat'] if restauration_source == 'candidat' else []) }}
register: restauration_temoins
changed_when: false
failed_when: false
when: restauration_action == 'temoins'
- name: Temoins
ansible.builtin.debug:
msg: "{{ (restauration_temoins.stdout_lines | default([])) + (restauration_temoins.stderr_lines | default([])) }}"
when: restauration_action == 'temoins'
- name: Temoins — verdict
ansible.builtin.assert:
that:
- restauration_temoins.rc in [0, 4]
fail_msg: >-
{{ 'ECART : une perte, ou une identite changee (voir ci-dessus).'
if restauration_temoins.rc == 1 else
'setops-restaurer temoins a echoue (code ' ~ restauration_temoins.rc ~ ') : '
~ 'client_backup est-il deploye ?' }}
quiet: true
when: restauration_action == 'temoins'
- name: Ecarter l'etat anterieur de ce jeu
ansible.builtin.command:
argv: [/usr/local/sbin/setops-restaurer, acter, "{{ restauration_jeu }}", abandonne]
changed_when: true
when: restauration_action == 'renoncer'

View file

@ -37,3 +37,20 @@
pre_tasks: *pre_sante
roles:
- client_sante
# LE RETRAIT, SUR LES HOTES SORTIS DU GROUPE (2026-10-03). Un role qu'on cesse d'appliquer
# laisse son minuteur derriere lui : les hyperviseurs du site ont pousse vers une adresse
# perimee pendant des semaines (voir `roles/client_sante/tasks/retirer.yml`).
#
# LE GROUPE EST NOMME, pas deduit (`all:!client_sante`) : un hyperviseur n'est pas une VM
# de la flotte, et un play qui le touche doit le dire. Chez un tenant, `hyperviseurs`
# n'existe pas et ce play ne trouve personne.
- name: Intégration client_sante — retrait sur les hyperviseurs qui n'y sont plus
hosts: hyperviseurs:!client_sante
become: true
gather_facts: false
tasks:
- name: Retirer le porteur de santé
ansible.builtin.include_role:
name: client_sante
tasks_from: retirer.yml

View file

@ -47,7 +47,7 @@
# l'URL absolue, et c'est LUI qui traduit le nom. La machine n'a donc besoin d'aucun
# DNS pour installer ses paquets — seulement d'une ADRESSE.
#
# C'EST DE LA FILIATION, PAS UNE RUSTINE (docs/filiation-emancipation.md). L'hébergeur
# C'EST DE LA FILIATION, PAS UNE RUSTINE (docs/conception/filiation-emancipation.md). L'hébergeur
# nourrit la première machine de son locataire le temps que celui-ci monte son propre
# cache. L'emprunt est DÉCLARÉ — l'intrant nomme l'amont — et il s'efface de lui-même :
# `client_artefacts` écrit `00-setops-artefacts`, qui trie APRÈS ce fichier-ci, donc
@ -78,6 +78,16 @@
when:
- artefacts_amorcage is defined
- artefacts_amorcage | string | length > 0
- not (artefacts_amorcage_direct | default(false) | bool)
# LA MACHINE QUI PORTE LE CACHE VA DROIT CHEZ DEBIAN (2026-10-13). Le fichier herite du
# gabarit vise un cache qui n'existe pas encore — le sien — ou une adresse perimee :
# l'inventaire le dit par `artefacts_amorcage_direct`, et on le retire.
- name: Retirer l'amorcage sur la machine qui porte le cache
ansible.builtin.file:
path: /etc/apt/apt.conf.d/00-setops-amorcage-artefacts
state: absent
when: artefacts_amorcage_direct | default(false) | bool
# RETIRER CE QUI DESIGNE UN CACHE QUI N'EXISTE PLUS (2026-09-10).
#

View file

@ -0,0 +1,19 @@
---
- name: Appliquer le groupe serveur_relais
hosts: serveur_relais
become: true
module_defaults:
ansible.builtin.apt:
lock_timeout: 300
gather_facts: true
pre_tasks:
- name: Vérifier que la cible est Debian
ansible.builtin.assert:
that:
- ansible_facts.distribution == "Debian"
fail_msg: "Ce playbook est prévu pour Debian."
roles:
- serveur_relais

View file

@ -1,7 +1,7 @@
---
# PROUVER QU'UN LIEN EST COUPÉ — l'instrument de la quatrième ligne.
#
# `docs/filiation-emancipation.md` décrit quatre temps, et n'en outillait que trois :
# `docs/conception/filiation-emancipation.md` décrit quatre temps, et n'en outillait que trois :
#
# le lien de filiation déclaré au plan le moteur
# le constat d'aptitude « tu pourrais » la machine INSTRUIT
@ -79,7 +79,7 @@
SERVICE doit valoir l'un de {{ emancipation_services.keys() | join(', ') }},
et cet écosystème doit DÉCLARER l'amont correspondant. Un emprunt qu'aucune
variable ne nomme ne peut pas être prouvé coupé — c'est la forme normée de
`docs/filiation-emancipation.md` qui rend cette preuve possible.
`docs/conception/filiation-emancipation.md` qui rend cette preuve possible.
# PAS de `run_once` : l'amont se lit dans les variables de CHAQUE hote. Un
# ecosysteme peut tres bien emprunter sur une machine et pas sur une autre — c'est
# meme le cas ici, ou seul le porteur du cache chaine vers celui du site. Verifier

View file

@ -0,0 +1,43 @@
---
# LE SITE ENREGISTRE LA CLE DE LECTURE DE CHAQUE LOCATAIRE SUR SON DEPOT (2026-10-11).
#
# Voir `docs/conception/depots-prives-conception.md` et `scripts/forge_cles.py`. Le script tourne SUR le
# runner du site : il y lit les faces que le site tire (et rien d'autre de ses locataires), et
# il y joint la forge. Le mot de passe d'administration vient de la voute du site, par
# l'environnement, et n'est jamais affiche.
#
# make forge-cles montre ce qui serait fait
# make forge-cles CONFIRMER=true l'ecrit, puis relit
- name: Les cles de lecture des locataires, sur la forge du site
hosts: serveur_ops_site
gather_facts: false
become: true
become_user: "{{ serveur_ops_utilisateur | default('setops') }}"
vars:
forge_cles_racine: "{{ serveur_ops_racine | default('/opt/setops') }}"
forge_cles_moteur: "{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}"
forge_cles_confirmer: "{{ lookup('env', 'CONFIRMER') == 'true' }}"
tasks:
- name: Exiger le mot de passe d'administration de la forge (voute du site)
ansible.builtin.assert:
that: vault_forgejo_admin | default('') | length > 0
fail_msg: "vault_forgejo_admin absent : la voute du site n'est pas chargee."
no_log: true
- name: Confronter les faces aux cles de la forge
ansible.builtin.command:
argv: "{{ ['python3', 'scripts/forge_cles.py']
+ (['--confirmer'] if (forge_cles_confirmer | bool) else []) }}"
chdir: "{{ forge_cles_racine }}/{{ forge_cles_moteur }}"
environment:
FORGEJO_ADMIN: "{{ vault_forgejo_admin }}"
PATH: "{{ forge_cles_racine }}/venv/bin:/usr/local/bin:/usr/bin:/bin"
# En simulation, le script ne fait que montrer (sans --confirmer) : il peut tourner.
check_mode: false
register: forge_cles
changed_when: "'ajoutee' in forge_cles.stdout or 'retiree' in forge_cles.stdout or 'rendu ' in forge_cles.stdout"
failed_when: forge_cles.rc not in [0]
- name: Ce que la forge porte
ansible.builtin.debug:
msg: "{{ forge_cles.stdout_lines }}"

View file

@ -0,0 +1,47 @@
---
# LES COMPTES DE LA FORGE DU SITE (2026-10-11, lot 3b).
#
# Voir `docs/conception/depots-prives-conception.md` et `scripts/forge_comptes.py`. Le script tourne SUR le
# runner du site : il y lit le plan du site (`mainteneurs`) et les faces de ses locataires, et
# il y joint la forge. Le mot de passe d'administration vient de la voute du site, par
# l'environnement, et n'est jamais affiche.
#
# make forge-comptes montre ce qui serait fait
# make forge-comptes CONFIRMER=true l'ecrit, puis relit
- name: Les comptes de la forge du site
hosts: serveur_ops_site
gather_facts: false
become: true
become_user: "{{ serveur_ops_utilisateur | default('setops') }}"
vars:
forge_comptes_racine: "{{ serveur_ops_racine | default('/opt/setops') }}"
forge_comptes_moteur: "{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}"
forge_comptes_confirmer: "{{ lookup('env', 'CONFIRMER') == 'true' }}"
tasks:
- name: Exiger le mot de passe d'administration de la forge (voute du site)
ansible.builtin.assert:
that: vault_forgejo_admin | default('') | length > 0
fail_msg: "vault_forgejo_admin absent : la voute du site n'est pas chargee."
no_log: true
- name: Confronter le plan du site aux comptes de la forge
ansible.builtin.command:
argv: "{{ ['python3', 'scripts/forge_comptes.py']
+ (['--confirmer'] if (forge_comptes_confirmer | bool) else []) }}"
chdir: "{{ forge_comptes_racine }}/{{ forge_comptes_moteur }}"
environment:
FORGEJO_ADMIN: "{{ vault_forgejo_admin }}"
PATH: "{{ forge_comptes_racine }}/venv/bin:/usr/local/bin:/usr/bin:/bin"
# En simulation, le script ne fait que montrer (sans --confirmer) : il peut tourner.
check_mode: false
register: forge_comptes
# LES LIGNES D'ACTION, PAS DES MOTS : « jeton a creer », dans le plan, contient « jeton »
# et faisait compter un changement alors que rien n'etait ecrit (2026-10-11).
changed_when: forge_comptes.stdout is search('(?m)^ (cree|collab|retire|equipe|jeton) ')
# 1 = un ecart DIT (un courriel a remplir, par exemple) : il s'affiche, il n'empeche pas
# le reste. 2 = le script a refuse de travailler.
failed_when: forge_comptes.rc not in [0, 1]
- name: Ce que la forge porte
ansible.builtin.debug:
msg: "{{ forge_comptes.stdout_lines }}"

View file

@ -61,6 +61,11 @@
url: >-
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
| default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }}
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
status_code: [200, 404]
loop: "{{ genome_compares }}"

View file

@ -22,6 +22,7 @@
#
# make genome-pousser tous les depots portes par le runner
# make genome-pousser DEPOT=<nom> un seul (le nom du depot sur la forge)
# make genome-pousser ARGS=--check ce que la poussee ferait, depot par depot, sans rien ecrire
#
# L'ECRITURE S'AUTHENTIFIE, LA LECTURE NON. `serveur_ops` clone en anonyme, par doctrine.
# Pousser est un autre acte : le runner s'y presente avec SA cle — celle que `serveur_ops`
@ -95,6 +96,181 @@
# depot par depot, et l'exploitant doit aller cliquer. La cible s'en charge : la
# declaration est idempotente (422 = deja la), et sa portee est exactement ce qu'on
# pousse, jamais plus large.
# UNE FORGE NEUVE, UN RUNNER NEUF (2026-10-13, premier site construit depuis zero). Le
# runner du site ne fabrique sa cle qu'APRES avoir clone le genome, et il ne peut cloner
# que ce que cette commande a pousse — qui exige sa cle. Personne n'allait au bout. On
# fabrique donc la cle ici si elle manque, A L'IDENTIQUE de `serveur_ops` (meme commande,
# meme commentaire, `creates:`) : le role la retrouvera et ne la refera pas.
- name: Assurer le répertoire SSH du runner
ansible.builtin.file:
path: "{{ genome_racine }}/.ssh"
state: directory
mode: "0700"
- name: Fabriquer la clé du runner si elle n'existe pas encore
ansible.builtin.command:
cmd: >-
ssh-keygen -t {{ serveur_ops_cle_type | default('ed25519') }} -N ''
-C "setops@{{ inventory_hostname }}.{{ domaine_interne }}"
-f {{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}
creates: "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}"
# L'ORGANISATION ET SES DEPOTS N'ETAIENT CREES PAR PERSONNE : au site de Chezlepro, une
# fois, a la main. Meme forme qu'elle : organisation PUBLIQUE ; le moteur et les modeles
# PUBLICS (le runner les clone en anonyme) ; la carte du site et les plans des
# locataires PRIVES (`serveur_ops_roles_prives`, lus en SSH par cle de deploiement).
- name: L'organisation du génome existe-t-elle ?
ansible.builtin.uri:
url: "https://forge.{{ domaine_interne }}/api/v1/orgs/{{ serveur_ops_forge_organisation | default('genome') }}"
validate_certs: false
status_code: [200, 404]
# UNE LECTURE, DONC AUSSI EN SIMULATION : sans elle, `--check` sautait la sonde et la
# tache suivante echouait sur un statut absent (2026-10-14).
check_mode: false
register: genome_org
- name: Créer l'organisation du génome
ansible.builtin.uri:
url: "https://forge.{{ domaine_interne }}/api/v1/orgs"
method: POST
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
body_format: json
body:
username: "{{ serveur_ops_forge_organisation | default('genome') }}"
visibility: public
status_code: 201
no_log: true
when: genome_org.status == 404
- name: Quels dépôts poussés existent déjà sur la forge ?
ansible.builtin.uri:
url: "https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation | default('genome') }}/{{ item.depot }}"
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
status_code: [200, 404]
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
no_log: true
check_mode: false
register: genome_depots_forge
# LA SIMULATION DIT CE QUE LA POUSSEE FERAIT, PUIS S'ARRETE (2026-10-14).
#
# Tout ce qui suit est `uri` en ecriture ou `command` : `--check` le saute, et le verdict
# final comparait alors une forge intacte a des colis jamais recus. On ne lit donc que la
# forge et le poste, on compare, et on rend la main avant la premiere ecriture.
- name: Simulation — la branche que porte la forge
ansible.builtin.uri:
url: >-
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
| default('genome') }}/{{ item.item.depot }}/branches/{{ item.item.branche | default('main') }}
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
status_code: [200, 404]
loop: "{{ genome_depots_forge.results }}"
loop_control:
label: "{{ item.item.depot }}"
register: genome_sim_forge
retries: 4
delay: 3
until: genome_sim_forge is succeeded
check_mode: false
no_log: true
when:
- ansible_check_mode
- item.status == 200
# `--left-right` : a gauche ce que la forge porte et que le poste n'a pas (le runner
# refusera, `--ff-only`), a droite ce qui serait pousse. rc 128 : le poste ne connait
# pas le commit de la forge — l'avance rapide est impossible a juger d'ici.
- name: Simulation — l'écart entre la forge et le poste
ansible.builtin.command:
argv:
- git
- -C
- "{{ genome_freres }}/{{ item.item.item.dest }}"
- rev-list
- --count
- --left-right
- "{{ item.json.commit.id }}...refs/heads/{{ item.item.item.branche | default('main') }}"
delegate_to: localhost
become: false
loop: "{{ genome_sim_forge.results }}"
loop_control:
label: "{{ item.item.item.depot }}"
register: genome_sim_ecart
check_mode: false
changed_when: false
failed_when: false
when:
- ansible_check_mode
- item.status | default(0) == 200
- name: Simulation — ce que la poussée ferait
vars:
genome_sim_lignes: >-
{%- set out = [] -%}
{%- if genome_org.status == 404 -%}
{%- set _ = out.append('organisation ' ~ (serveur_ops_forge_organisation | default('genome')) ~ ' : à créer') -%}
{%- endif -%}
{%- for r in genome_sim_ecart.results -%}
{%- set d = r.item.item.item.depot -%}
{%- if r.item.item.status == 404 -%}
{%- set _ = out.append(d ~ ' : dépôt à créer, puis tout poussé') -%}
{%- elif r.item.status | default(0) == 404 -%}
{%- set _ = out.append(d ~ ' : dépôt vide, tout poussé') -%}
{%- elif r.rc != 0 -%}
{%- set _ = out.append(d ~ ' : la forge porte ' ~ r.item.json.commit.id[:7]
~ ', inconnu du poste — avance rapide impossible à juger d ici') -%}
{%- else -%}
{%- set lr = r.stdout.split() | map('int') | list -%}
{%- if lr[0] > 0 -%}
{%- set _ = out.append(d ~ ' : la forge a ' ~ lr[0] ~ ' commit(s) absent(s) du poste — le runner REFUSERA (--ff-only)') -%}
{%- elif lr[1] == 0 -%}
{%- set _ = out.append(d ~ ' : à jour (' ~ r.item.json.commit.id[:7] ~ ')') -%}
{%- else -%}
{%- set _ = out.append(d ~ ' : ' ~ r.item.json.commit.id[:7] ~ ' → ' ~ lr[1] ~ ' commit(s) en avance rapide') -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{{ out }}
ansible.builtin.debug:
msg: "{{ genome_sim_lignes }}"
when: ansible_check_mode
- name: Simulation — s'arrêter avant la première écriture
ansible.builtin.meta: end_host
when: ansible_check_mode
- name: Créer les dépôts qui manquent
ansible.builtin.uri:
url: "https://forge.{{ domaine_interne }}/api/v1/orgs/{{ serveur_ops_forge_organisation | default('genome') }}/repos"
method: POST
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
body_format: json
body:
name: "{{ item.item.depot }}"
private: "{{ item.item.role in (serveur_ops_roles_prives | default(['instance', 'tenant', 'hebergeur'])) }}"
default_branch: "{{ item.item.branche | default('main') }}"
auto_init: false
status_code: 201
loop: "{{ genome_depots_forge.results }}"
loop_control:
label: "{{ item.item.depot }}"
no_log: true
when: item.status == 404
- name: Lire la clé publique du runner
ansible.builtin.slurp:
src: "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}.pub"
@ -160,6 +336,7 @@
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.dest }}"
register: genome_emballe
changed_when: true
- name: Porter les colis jusqu'au runner
@ -215,11 +392,16 @@
- name: Demander à la forge ce qu'elle porte maintenant
# On interroge son API plutôt que `git ls-remote` : c'est la forge elle-même qui
# répond, et la lecture est anonyme — aucun justificatif n'entre ici.
# répond. Lecture authentifiée depuis le 2026-10-11 (dépôts privés).
ansible.builtin.uri:
url: >-
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
| default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }}
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
status_code: [200]
loop: "{{ genome_a_pousser }}"
@ -255,6 +437,50 @@
label: "{{ item.depot }}"
index_var: idx
# LES RELEASES AUSSI (2026-10-07). Le colis ne portait que la branche : `v2026.08.21` et
# `v2026.10.07` manquaient a la forge du site. Il porte desormais les etiquettes ; on
# demande a la forge si elle a TOUTES celles du poste. 404 = aucune etiquette sur ce
# depot, ce qui est juste quand le poste n'en a pas non plus.
- name: Demander à la forge les étiquettes qu'elle porte
ansible.builtin.uri:
url: >-
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
| default('genome') }}/{{ item.depot }}/git/refs/tags
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
status_code: [200, 404]
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
register: genome_forge_etiquettes
retries: 4
delay: 3
until: genome_forge_etiquettes is succeeded
changed_when: false
- name: La forge porte-t-elle toutes les releases du poste ?
vars:
genome_etiquettes_poste: "{{ (genome_emballe.results[idx].stdout | from_json).etiquettes | default([]) }}"
genome_etiquettes_forge: >-
{{ (genome_forge_etiquettes.results[idx].json | default([]) if genome_forge_etiquettes.results[idx].status == 200 else [])
| map(attribute='ref') | map('regex_replace', '^refs/tags/', '') | list }}
ansible.builtin.assert:
that:
- genome_etiquettes_poste | difference(genome_etiquettes_forge) | length == 0
fail_msg: >-
{{ item.depot }} : la forge n'a pas
{{ genome_etiquettes_poste | difference(genome_etiquettes_forge) | join(', ') }}.
success_msg: >-
{{ item.depot }} : {{ genome_etiquettes_poste | length }} etiquette(s) du poste, toutes sur la forge.
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
index_var: idx
# LE RUNNER DU SITE AVANCE SON PROPRE CLONE, ET SA CONSOLE NE LE SAIT PAS (2026-09-20).
#
# C'est LUI qui pousse, donc il doit d'abord recevoir : ce playbook fait avancer le

View file

@ -46,7 +46,7 @@ instance/inventories/production/group_vars/all/vault.yml
`make config` la sème depuis le gabarit et y place le token. Le clonage lit cette voûte
unifiée. (`proxmox.vault.yml` n'est plus lue — retirée le 2026-08-03.) Détail des
paramètres : [`docs/config-proxmox.md`](../../docs/config-proxmox.md).
paramètres : [`docs/exploitation/config-proxmox.md`](../../docs/exploitation/config-proxmox.md).
Créer un clone Debian depuis le modèle :

View file

@ -21,13 +21,17 @@
# vit dans le depot de l'hebergeur, que le symlink `underlay.yml` designe deja
# (D-14/D-17) : on en DERIVE le chemin plutot que de redeclarer l'hebergeur. Absent
# (depot sans underlay monte), le `stat` echoue simplement et rien n'est charge.
setops_proxmox_hebergeur: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/proxmox-hebergeur.yml"
# LA CARTE DESIGNEE (2026-10-13) : `SETOPS_UNDERLAY` quand il est pose, le symlink sinon.
# Lire le symlink seul aurait clone les machines du site de TechnoLibre sur le cluster
# de Chezlepro, avec ses jetons — l'inventaire, lui, suivait deja la variable.
setops_carte_underlay: "{{ lookup('ansible.builtin.env', 'SETOPS_UNDERLAY') | default(playbook_dir ~ '/../../underlay.yml', true) }}"
setops_proxmox_hebergeur: "{{ setops_carte_underlay | realpath | dirname }}/proxmox-hebergeur.yml"
# LA VOUTE DE L'UNDERLAY — les secrets du MONDE PHYSIQUE (jeton d'API du cluster).
# Meme derivation que le fichier ci-dessus : le symlink `underlay.yml` designe deja
# l'hebergeur, on n'a rien a redeclarer. Depuis le 2026-08-22 ces secrets ne vivent
# plus dans la voute d'un tenant : recopies chez chacun, ils ne pouvaient plus etre
# revoques isolement (docs/frontiere-physique-virtuel.md).
setops_voute_underlay: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/underlay.vault.yml"
# revoques isolement (docs/conception/frontiere-physique-virtuel.md).
setops_voute_underlay: "{{ setops_carte_underlay | realpath | dirname }}/underlay.vault.yml"
tasks:
- name: Verifier les fichiers de variables Proxmox optionnels
@ -140,7 +144,7 @@
# L'exploitant doit garder son acces a la machine qu'il vient de creer : ecraser sa
# cle par celle du runner rendrait le runner seul maitre du terrain, ce qui est
# exactement l'inverse de la doctrine (c'est l'humain qui arme, cf.
# docs/filiation-emancipation.md). Deux cles, deux lignes, et cloud-init les pose
# docs/conception/filiation-emancipation.md). Deux cles, deux lignes, et cloud-init les pose
# toutes les deux.
#
# Elle n'arrive ICI que pour le runner d'un tenant : `inventory_host.py` la rend vide

View file

@ -22,7 +22,11 @@
# vit dans le depot de l'hebergeur, que le symlink `underlay.yml` designe deja
# (D-14/D-17) : on en DERIVE le chemin plutot que de redeclarer l'hebergeur. Absent
# (depot sans underlay monte), le `stat` echoue simplement et rien n'est charge.
setops_proxmox_hebergeur: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/proxmox-hebergeur.yml"
# LA CARTE DESIGNEE (2026-10-13) : `SETOPS_UNDERLAY` quand il est pose, le symlink sinon.
# Lire le symlink seul aurait clone les machines du site de TechnoLibre sur le cluster
# de Chezlepro, avec ses jetons — l'inventaire, lui, suivait deja la variable.
setops_carte_underlay: "{{ lookup('ansible.builtin.env', 'SETOPS_UNDERLAY') | default(playbook_dir ~ '/../../underlay.yml', true) }}"
setops_proxmox_hebergeur: "{{ setops_carte_underlay | realpath | dirname }}/proxmox-hebergeur.yml"
# cibles_vmid : liste d'entiers, passée en -e (sinon dérivée des hôtes actifs).
cibles_vmid: "{{ groups['hotes_actifs'] | map('extract', hostvars, 'proxmox_vmid') | map('int') | list }}"

View file

@ -11,6 +11,12 @@
- name: Couche socle — groupe serveur_durci
import_playbook: groupes/serveur_durci.yml
# --- couche depot_etat ---
- name: Couche depot_etat — groupe serveur_backup
import_playbook: groupes/serveur_backup.yml
- name: Couche depot_etat — groupe serveur_backup_site
import_playbook: groupes/serveur_backup_site.yml
# --- couche pki_racine ---
- name: Couche pki_racine — groupe serveur_step_ca
import_playbook: groupes/serveur_step_ca.yml
@ -50,8 +56,6 @@
import_playbook: groupes/serveur_nginx.yml
- name: Couche services — groupe serveur_rspamd
import_playbook: groupes/serveur_rspamd.yml
- name: Couche services — groupe serveur_backup
import_playbook: groupes/serveur_backup.yml
- name: Couche services — groupe serveur_artefacts
import_playbook: groupes/serveur_artefacts.yml
- name: Couche services — groupe serveur_cache_site
@ -60,8 +64,8 @@
import_playbook: groupes/serveur_forge_site.yml
- name: Couche services — groupe serveur_resolveur_site
import_playbook: groupes/serveur_resolveur_site.yml
- name: Couche services — groupe serveur_backup_site
import_playbook: groupes/serveur_backup_site.yml
- name: Couche services — groupe serveur_relais
import_playbook: groupes/serveur_relais.yml
- name: Couche services — groupe serveur_dns_public
import_playbook: groupes/serveur_dns_public.yml
- name: Couche services — groupe serveur_resolveur

View file

@ -11,11 +11,23 @@
gather_facts: false
become: false
tasks:
- name: Interroger l'API des cibles actives
# UN PROMETHEUS QUI VIENT DE REDEMARRER N'A PAS ENCORE RELEVE SES CIBLES (2026-10-08).
# Depuis qu'obs-01 est sauvegarde, son premier depot (copie a froid, en fin de
# deploiement) relance Prometheus : a la reconstruction de Technolibre, la recette l'a
# interroge 18 s apres, et deux cibles n'avaient pas encore de releve reussi — saines
# quelques minutes plus tard. On redemande donc, une minute au plus, avant de juger :
# une vraie panne echoue toujours, une minute plus tard.
- name: Interroger l'API des cibles actives (jusqu'a ce que toutes soient relevees, une minute au plus)
ansible.builtin.uri:
url: "http://localhost:9090/api/v1/targets?state=active"
return_content: true
register: prom_targets
until: >-
((prom_targets.content | default('{}') | from_json).get('data', {}).get('activeTargets', [])
| rejectattr('health', 'equalto', 'up') | list | length == 0)
or (prom_targets.attempts | default(0) | int >= 6)
retries: 6
delay: 10
- name: Analyser up / down
vars:

View file

@ -532,7 +532,7 @@
{"hash":"e17ae54fda0884f2b74830bb92d7069dc2c10c59","date":"2026-07-02","subject":"serveur_postfix : résolution du plancher /etc/hosts (native + chroot)"},
{"hash":"5846a08667bfbc712078f3520a9ca08dde56636d","date":"2026-07-02","subject":"serveur_dovecot : remise LMTP fonctionnelle — flux courriel interne prouvé"},
{"hash":"8df1db3b4740ecb1d34082fc13e0cfcd4231c3eb","date":"2026-07-02","subject":"serveur_rspamd (rspamd 3.x) : antispam + DKIM en milter Postfix — prouvé"},
{"hash":"defe16d1769d17c2d87776f10deef65219c2e35c","date":"2026-07-02","subject":"docs: bilan des pouvoirs de Set-OPS (docs/pouvoirs-set-ops.md)"},
{"hash":"defe16d1769d17c2d87776f10deef65219c2e35c","date":"2026-07-02","subject":"docs: bilan des pouvoirs de Set-OPS (docs/projet/pouvoirs-set-ops.md)"},
{"hash":"60b7de980f3763489c547a9787230844f35b1b47","date":"2026-07-02","subject":"Licence : CC BY-NC-SA 4.0 → AGPLv3 (verbatim) + modèle libre/services"},
{"hash":"4454e1e18bff73eed1290b5135db4a3fedb23b3c","date":"2026-07-03","subject":"docs: conception des bindings (relations app/base/serveur/domaine)"},
{"hash":"d95b26c2430012f7e665617212cc87e56282a6c1","date":"2026-07-03","subject":"bindings Phase 1 : résolveur de liens dans instancier (relations déclaratives)"},

View file

@ -6,7 +6,7 @@ seule fois, pour que l'exploitant puisse prendre la main. Puis se retire.
## Ce qu'il ne fait pas
Ce n'est **pas** un gestionnaire de comptes. Après son passage, créer, suspendre ou
réaffecter relève d'une personne — pas du dépôt (D-67, `docs/autorisation.md`).
réaffecter relève d'une personne — pas du dépôt (D-67, `docs/exploitation/autorisation.md`).
## Idempotence par existence, pas par conformité
@ -39,6 +39,6 @@ connexion à l'annuaire est dérivée par `resoudre_annuaire`, jamais écrite.
## Après
`docs/autorisation.md` §6 est le runbook de reprise : où administrer quoi, comment se
`docs/exploitation/autorisation.md` §6 est le runbook de reprise : où administrer quoi, comment se
rouvrir si on se ferme dehors, et ce qu'il faut régénérer pour que la livraison soit un
vrai transfert.

View file

@ -8,7 +8,7 @@
# promu, deplace — c'est le droit de celui qui exploite. Reconcilier effacerait
# le compte cree la veille pour un nouvel employe.
#
# Voir docs/autorisation.md (§6 = runbook de reprise).
# Voir docs/exploitation/autorisation.md (§6 = runbook de reprise).
amorcage_acces_actif: true

View file

@ -1,6 +1,6 @@
---
# Amorcage des acces — D-67. Voir defaults/main.yml pour la doctrine, et
# docs/autorisation.md §6 pour le runbook de reprise.
# docs/exploitation/autorisation.md §6 pour le runbook de reprise.
- name: Résoudre la connexion à l'annuaire
ansible.builtin.include_role:
@ -23,7 +23,7 @@
fail_msg: >-
`vault_sysadmin_amorcage` et `vault_openldap_admin` sont requis (voûte).
Le premier est un jeton d'amorçage à usage unique — pas un mot de passe
permanent : voir docs/autorisation.md §6.
permanent : voir docs/exploitation/autorisation.md §6.
when: amorcage_acces_actif | bool
# --- LE CŒUR DU RÔLE : on REGARDE avant d'agir --------------------------------
@ -54,7 +54,7 @@
msg: >-
Compte `{{ amorcage_acces_uid }}` déjà présent : Set-OPS n'y touche pas.
Les habilitations sont gouvernées par une personne (D-67) — voir
docs/autorisation.md §6.
docs/exploitation/autorisation.md §6.
when:
- amorcage_acces_actif | bool
- amorcage_acces_deja_fait | bool
@ -186,7 +186,7 @@
(amorcage_acces_reset_possible | bool)
| ternary('Changement FORCÉ à la première ouverture (ppolicy actif).',
'ATTENTION : rien ne force le changement — l overlay ppolicy n est pas chargé. '
~ 'Le changer à la main dès la première connexion (docs/autorisation.md §6).'),
~ 'Le changer à la main dès la première connexion (docs/exploitation/autorisation.md §6).'),
'À partir d ici, Set-OPS ne touche plus aux habilitations (D-67).'
] }}
when:

View file

@ -4,3 +4,15 @@
name: auditd
state: restarted
failed_when: false
# REDEMARRER `auditd` NE RECHARGE PAS LES REGLES (mesure du 2026-10-03, Debian 13).
#
# Elles sont chargees par `audit-rules.service` (`augenrules --load`), une unite a part
# dont `auditd` depend — et qu'un redemarrage d'`auditd` ne rejoue pas. Le handler
# ci-dessus laissait donc le fichier juste et le noyau sur les regles du dernier demarrage.
#
# SANS `failed_when: false` : une regle que le noyau refuse doit se voir ici, pas au
# prochain redemarrage de la machine.
- name: Recharger les regles auditd
ansible.builtin.command: augenrules --load
changed_when: true

View file

@ -15,7 +15,9 @@
path: "/etc/audit/rules.d/{{ item }}"
state: absent
loop: "{{ auditd_fichiers_perimes }}"
notify: Restart auditd
notify:
- Restart auditd
- Recharger les regles auditd
when: auditd_enabled | bool
# LA RETENTION EST UN CHOIX, DONC ELLE S'ECRIT (2026-09-11). Voir defaults/main.yml pour
@ -48,7 +50,9 @@
owner: root
group: root
mode: "0640"
notify: Restart auditd
notify:
- Restart auditd
- Recharger les regles auditd
when: auditd_enabled | bool
# `failed_when: false` A CACHE LA PANNE PENDANT TOUT UN DEPLOIEMENT (2026-08-30).

View file

@ -36,6 +36,20 @@
-w /etc/step-ca/ -p wa -k pki-autorite
{% endif %}
# L'EXPORTATEUR DE METRIQUES LIT L'HORLOGE, IL NE LA REGLE PAS (2026-10-03).
#
# `adjtimex` sert aux deux : regler ET lire. Le collecteur `timex` de `node_exporter` le lit
# a chaque collecte, et la regle `time-change` ci-dessous ne peut pas distinguer — le mode
# est dans une structure pointee, hors de portee d'un filtre `-F`. Mesure au site : 4
# evenements par minute, 35 % de toute la piste d'audit d'une VM, pour un appel en lecture.
#
# UNE REGLE `never` ETROITE, ET DEVANT : l'audit retient la PREMIERE regle qui correspond.
# Elle ne vise que `adjtimex`, que ce binaire-la : `settimeofday`/`clock_settime` restent
# audites pour lui aussi, et chrony — qui regle vraiment — reste entierement audite.
# Eprouve avant d'ecrire : 4 evenements/min sans elle, 0 avec ; et `auditctl` accepte un
# `exe=` absent, donc la ligne ne casse pas le chargement d'une machine sans exportateur
# (ce que ferait un `-w` vers un chemin absent, voir plus haut).
-a never,exit -F arch=b64 -S adjtimex -F exe=/usr/bin/prometheus-node-exporter
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
-a always,exit -F arch=b64 -S sethostname,setdomainname -k system-locale

View file

@ -9,7 +9,7 @@
# dont le demon depend. Une sonde qui aurait compte les regles aurait dit « vert » pendant
# toute la panne. On mesure donc la COLLECTE — le service, puis l'ecriture.
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Contrat : docs/conception/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `auditd_sonde_age_max` (le baisser a 1).
set -uo pipefail

View file

@ -14,7 +14,7 @@
# tombe en panne de l'interieur, avec des symptomes qui ressemblent a tout sauf a une
# horloge.
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Contrat : docs/conception/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `chrony_sonde_ecart_max` (le baisser a 0).
set -uo pipefail

View file

@ -1,6 +1,6 @@
---
# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le
# raisonnement, et docs/decisions-architecture.md (D-33).
# raisonnement, et docs/projet/decisions-architecture.md (D-33).
integration:
# DE QUI CETTE INTEGRATION DEPEND, ET POURQUOI L'ORDRE COMPTE.
#

Some files were not shown because too many files have changed in this diff Show more