durcissement : cloud-init nait avec la VM et ne lui survit pas
cloud-init n est pas un logiciel d installation : c est une SOURCE DE VERITE EXTERNE. Il se reveille a chaque demarrage et relit le lecteur attache par l hyperviseur, qui peut redefinir comptes, cles SSH, mots de passe et reseau. Sur une machine que le plan possede, c est un second maitre — que le plan ne decrit pas, que make valider ne mesure pas, et qui parle en premier. Sa tache est finie a la premiere seconde : c est parce qu il a REUSSI a poser l adresse et les cles qu Ansible a pu entrer. TROIS MOITIES, ET ELLES SE DEFONT SEPAREMENT. - le GABARIT le garde : sans lui un clone n a ni adresse ni nom ; - le SOCLE ne l installe plus : le garder produisait un va-et-vient a chaque deploiement, deux changed par passage, idempotence perdue ; - le DURCISSEMENT le retire (roles/cloud_init_retrait, en dernier). P63 garde les trois, plus le CONTENU du role : une coquille vide passerait les trois premiers controles sans rien fermer. Quatre controles negatifs rejoues. CE QUI REND LE RETRAIT SUR EST MESURE, PAS SUPPOSE (obs-01, 2026-09-09) : /etc/network/interfaces.d/50-cloud-init n appartient a aucun paquet — dpkg -S ne le trouve pas — et le postrm ne le nomme jamais, meme en purge. L adresse survit. Le role le verifie quand meme, avant et apres, et n accuse que si le retrait l a emporte : une VM qui perd ce fichier ne se plaint pas, elle repart sans adresse et plus personne ne peut entrer. DEUX CHOIX DITS FRANCHEMENT. cloud-guest-utils reste (growpart : ni service, ni port, ni source de donnees). Les ~29 paquets orphelins ne sont pas retires par defaut : autoremove deciderait a partir des drapeaux dpkg, et un durcissement ne doit pas pouvoir surprendre. NON DEPLOYE : le code est ecrit, valide et prouve ; il n a pas ete applique a la flotte. Essai a blanc sur obs-01 : cloud-init a retirer, configuration reseau intacte. make prouver : CONFORME, 62 OK, 0 echec, 1 saute (63 preuves). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
765d97b5c9
commit
53d7b4c4c2
13 changed files with 439 additions and 9 deletions
14
AGENTS.md
14
AGENTS.md
|
|
@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
|||
## Écrire, puis relire (D-68)
|
||||
|
||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||
C'est aussi ce que font les 62 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
C'est aussi ce que font les 63 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||
dépôt annonce.**
|
||||
|
||||
|
|
@ -547,8 +547,8 @@ Il peut contenir :
|
|||
- compte technique `ansible` ;
|
||||
- sudo NOPASSWD pour `ansible` lorsque requis ;
|
||||
- `qemu-guest-agent` ;
|
||||
- `cloud-init` ;
|
||||
- `cloud-guest-utils` ;
|
||||
- `cloud-init` — **au gabarit seulement** : retiré par `serveur_durci` une fois la VM née (D-85) ;
|
||||
- `cloud-guest-utils` (`growpart`) — conservé : ni service, ni source de données ;
|
||||
- chrony ;
|
||||
- outils de diagnostic de base ;
|
||||
- durcissement raisonnable ;
|
||||
|
|
@ -636,6 +636,14 @@ Proxmox + cloud-init : identité initiale de la VM
|
|||
Set-OPS + Ansible : configuration réelle du serveur
|
||||
```
|
||||
|
||||
**Et cloud-init ne survit pas à cette première seconde** (D-85, 2026-09-09). Il se réveille
|
||||
à *chaque* démarrage et relit le lecteur attaché par l'hyperviseur — lequel peut redéfinir
|
||||
comptes, clés SSH, mots de passe et réseau. Sur une machine que le plan possède, c'est un
|
||||
**second maître**, que le plan ne décrit pas. Le groupe `serveur_durci` le retire donc
|
||||
(`cloud_init_retrait`), et le socle ne l'installe plus : le garder aux deux endroits
|
||||
produisait un va-et-vient à chaque déploiement. **Le gabarit, lui, le garde** — sans lui un
|
||||
clone n'a ni adresse ni nom. **P63** garde les trois moitiés.
|
||||
|
||||
---
|
||||
|
||||
## Nettoyage avant template
|
||||
|
|
|
|||
77
CHANGELOG.md
77
CHANGELOG.md
|
|
@ -1,5 +1,82 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-09 — cloud-init nait avec la VM et ne lui survit pas
|
||||
|
||||
**63 preuves (P01-P63). `make prouver` : CONFORME, 62 OK, 0 echec, 1 saute.**
|
||||
|
||||
### Le second maitre
|
||||
|
||||
cloud-init n'est pas un logiciel d'installation : c'est une **source de verite externe**.
|
||||
Il ne s'arrete pas apres la premiere seconde — il se reveille a CHAQUE demarrage et relit
|
||||
le lecteur cloud-init attache par l'hyperviseur, lequel peut redefinir les comptes, les
|
||||
cles SSH autorisees, les mots de passe et le reseau.
|
||||
|
||||
Sur une machine que le plan possede desormais, c'est un second maitre : le plan ne le
|
||||
decrit pas, `make valider` ne le mesure pas, et il parle en premier.
|
||||
|
||||
Sa tache est pourtant finie a la premiere seconde. C'est precisement parce qu'il a REUSSI
|
||||
a poser l'adresse, le nom et les cles d'hote qu'Ansible a pu entrer.
|
||||
|
||||
### Trois moities, et elles se defont separement
|
||||
|
||||
1. le **gabarit** le garde. Sans lui, un clone n'a ni adresse ni nom : il ne nait pas.
|
||||
2. le **socle** ne l'installe plus. Le garder produisait un va-et-vient a chaque
|
||||
deploiement — le socle installe, le durcissement retire, deux `changed` par passage,
|
||||
l'idempotence perdue et `make valider` bruyant pour rien.
|
||||
3. le **durcissement** le retire (`roles/cloud_init_retrait`, dernier role de
|
||||
`serveur_durci`, apres que tout le reste soit pose).
|
||||
|
||||
Une seule des trois qui bouge et la decision devient son contraire en silence : un gabarit
|
||||
sans cloud-init donne des VM mortes ; un socle qui le reinstalle rend le pouvoir a chaque
|
||||
passage ; un durcissement qui ne le retire plus laisse le second maitre en place. **P63**
|
||||
garde les trois, plus le contenu du role — un role vide passerait les trois premiers
|
||||
controles sans rien fermer. Les quatre controles negatifs ont ete rejoues.
|
||||
|
||||
### Ce qui rend le retrait sur est MESURE, pas suppose
|
||||
|
||||
L'adresse d'une VM vit dans `/etc/network/interfaces.d/50-cloud-init`. Le risque evident
|
||||
etait que le retrait l'emporte — une machine qui perd ce fichier ne se plaint pas : elle
|
||||
repart au prochain demarrage sans adresse, et plus personne ne peut entrer pour le
|
||||
constater. Mesure du 2026-09-09 sur `obs-01` :
|
||||
|
||||
dpkg -S /etc/network/interfaces.d/50-cloud-init -> aucun paquet ne le possede
|
||||
/var/lib/dpkg/info/cloud-init.postrm -> ne nomme jamais interfaces.d
|
||||
|
||||
Le fichier survit donc, meme en `purge`, et `interfaces` fait toujours
|
||||
`source interfaces.d/*`. Son en-tete annonce que les modifications « ne persistent pas au
|
||||
redemarrage » : c'etait vrai TANT QUE cloud-init pouvait le reecrire. Le paquet parti,
|
||||
plus personne ne le reecrit.
|
||||
|
||||
**Le role le verifie quand meme**, avant et apres, et n'accuse que si le retrait l'a
|
||||
emporte — une machine qui n'a jamais eu ce fichier ne doit pas faire echouer le
|
||||
durcissement. Essai a blanc sur `obs-01` : `cloud-init*` a retirer, configuration reseau
|
||||
intacte, drapeau pose.
|
||||
|
||||
### Deux choix dits franchement
|
||||
|
||||
**`cloud-guest-utils` reste.** C'est `growpart` : ni service, ni port, ni source de
|
||||
donnees. Le retirer ne fermerait aucune surface, et il faudrait le reinstaller au premier
|
||||
agrandissement de disque.
|
||||
|
||||
**Les orphelins ne sont pas retires par defaut.** Le retrait laisse ~29 paquets qui
|
||||
n'etaient la que pour cloud-init (`python3-jsonschema`, `python3-jinja2`,
|
||||
`python3-oauthlib`...). Ce sont des bibliotheques : elles pesent, elles n'ouvrent rien.
|
||||
`autoremove` calculerait exactement quoi retirer — a partir des drapeaux « installe
|
||||
manuellement » de dpkg, et une machine dont ces drapeaux ont derive y perdrait autre
|
||||
chose. Un durcissement ne doit pas pouvoir surprendre. `cloud_init_retrait_autoremove`
|
||||
existe pour qui veut, en connaissance de cause.
|
||||
|
||||
### Et un drapeau, pour le retour par dependance
|
||||
|
||||
`cloud-init` peut revenir en recommandation d'un autre paquet.
|
||||
`/etc/cloud/cloud-init.disabled` est lu par cloud-init lui-meme au demarrage et l'arrete
|
||||
avant qu'il ne lise la moindre source de donnees.
|
||||
|
||||
### Non deploye
|
||||
|
||||
Le code est ecrit, valide et prouve ; **il n'a pas ete applique a la flotte**. Retirer un
|
||||
paquet sur quatorze VM de production est une action a confirmer, pas a supposer.
|
||||
|
||||
## 2026-09-08 (4) — Les SIX registres ont un formulaire genere
|
||||
|
||||
**62 preuves. `make prouver` : CONFORME, 61 OK, 0 echec, 1 saute.**
|
||||
|
|
|
|||
99
docs/audit/preuve-2026-09-09.md
Normal file
99
docs/audit/preuve-2026-09-09.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Preuve de conformite — Set-OPS — 2026-09-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** : `/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 (62 OK · 0 echec · 1 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
| # | Preuve | Affirmations | Statut | Detail |
|
||||
|---|---|---|---|---|
|
||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
||||
| 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) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : 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 : 40 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 38 rôles, 99 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 : 23 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 28 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) : adressage 100% derive du seed index. |
|
||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 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 : 13 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, 3 tenant(s), 50 groupe(s), 80 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 5 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, 34 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 32 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 21, 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, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 58 scripts expliques et atteignables, 115 cibles make documentees, 66 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (131 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (37 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 43 document(s) declarent leur lecteur (35 genere(s) exempte(s)). |
|
||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 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` (9 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 : 39 role(s) serveur/client tous nommes, 40 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, 27 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 : 54 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 (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 104 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 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 89 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 (117 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 169 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 — 15 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 | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
|
||||
| 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 (63 preuves, 66 roles, 40 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 `2887b57` (publie le 2026-09-08). |
|
||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 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:5, 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 |
|
||||
|
||||
## 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-09._
|
||||
|
|
@ -23,12 +23,12 @@ README de rôles). Cette page comble ces deux trous.
|
|||
|
||||
| Ce qu'on compte | Combien | Comment on le mesure |
|
||||
|---|---|---|
|
||||
| rôles | 65 | `roles/*/` |
|
||||
| README de rôles | 65 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||
| rôles | 66 | `roles/*/` |
|
||||
| README de rôles | 66 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||
| documents | 39 | `docs/*.md` |
|
||||
| pièces d'audit | 39 | `docs/audit/*` |
|
||||
| pièces d'audit | 40 | `docs/audit/*` |
|
||||
| unités de wiki | 27 | `wiki/*.md` |
|
||||
| décisions en vigueur | 81 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
||||
| décisions en vigueur | 82 | 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)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ sont les seules vérifiables.
|
|||
| **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. Ce qu'il devait éliminer — le SPOF `eregion`, hors flotte — n'a PAS été éliminé mais **promu** : le poste y pousse, la forge du site en tire. Cette dette appartient désormais au SITE, et la nommer est le minimum : *un objectif qu'on abandonne sans le dire devient un objectif qu'on croit atteint* | `OPS-Patient0/README.md`, `docs/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 la première et **abaisse la seconde de trois copies vivantes à deux** (`eregion`, la forge du site). Ce qu'il devait éliminer — le SPOF `eregion` — reste entier, et sans lui il n'y a plus de miroir indépendant pour l'absorber. **Son plan reste sur disque et la fédération lui réserve toujours l'index 29** : tant que ce n'est pas tranché, le site ouvre SSH, apt, DNS et HTTPS à `10.29.0.0/16` — un périmètre vide | `OPS-Patient0/`, `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**. À chaque démarrage il relit le lecteur attaché par l'hyperviseur, qui 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** — que le plan ne décrit pas, que `make valider` ne mesure pas, et qui gagne parce qu'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. **La décision a trois moitiés, et elles 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`). **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 | `roles/cloud_init_retrait/`, `serveur_durci.yml` | **P63** |
|
||||
| **D-81** | **La forge du SITE fait autorité pour le génome.** Toute autre copie — y compris celle d'où le moteur a été poussé jusqu'ici — est un **miroir**. Le poste de l'exploitant ne route pas jusqu'à elle : c'est le **runner du site** qui publie, par `make genome-pousser` | un écosystème se reproduit depuis la forge de son site : c'est de là qu'il clone son moteur, ses plans, ses modèles. Si l'autorité est ailleurs, cette forge devient un cache qu'on croit à jour — et le 2026-08-26 elle était **quatre commits en arrière** sans que rien ne le signale, dont le correctif qui désarme le pare-feu Proxmox. **Un écosystème qui se reproduit depuis une forge en retard reproduit ses défauts.** Le poste n'a de patte que sur l'administration, et on ne perce pas de chemin pour lui : le runner existe pour ce travail | `playbooks/maintenance/genome_pousser.yml`, `scripts/genome_colis.py`, `Makefile` §genome-pousser | — |
|
||||
| **D-13** | Un **hébergeur** sert plusieurs **tenants** et a son tenant par défaut | Chezlepro est les deux à la fois, ce qui masquait la distinction | `frontiere-opnsense.md` §2 | — |
|
||||
| **D-14** | `underlay.yml` appartient à l'**hébergeur**, monté par symlink | ce sont ses commutateurs, ses câbles ; le moteur est générique, un tenant n'en possède pas | `sdn-evpn.md`, `underlay.yml.example` | — |
|
||||
|
|
|
|||
|
|
@ -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 62 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
`scripts/prouver.py` porte 63 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.
|
||||
|
|
|
|||
|
|
@ -146,7 +146,12 @@
|
|||
- hosts_statiques
|
||||
- common_packages
|
||||
- qemu_guest_agent
|
||||
- cloud_init
|
||||
# `cloud_init` N'EST PLUS APPLIQUE ICI (2026-09-09). Il reste au GABARIT, qui en a
|
||||
# besoin : il est le seul chemin vers la premiere seconde d'un clone (P56). Mais sur
|
||||
# une machine deja nee, le reinstaller n'a plus d'objet — et le durcissement le
|
||||
# retire juste apres. Les garder tous les deux produisait un va-et-vient a chaque
|
||||
# deploiement : le socle installe, le durcissement retire, deux `changed` par passage,
|
||||
# et l'idempotence perdue. P63 garde les trois moities de cette decision.
|
||||
- sudo_ansible
|
||||
- chrony
|
||||
- ssh_baseline
|
||||
|
|
|
|||
|
|
@ -29,3 +29,13 @@
|
|||
- journald
|
||||
- ssh_hardening
|
||||
- nftables_baseline
|
||||
# EN DERNIER, ET C'EST LE POINT. cloud-init a donne a la VM son adresse, son nom et
|
||||
# ses cles d'hote a la premiere seconde ; c'est parce qu'il a REUSSI qu'Ansible a pu
|
||||
# entrer. Mais il se reveille a CHAQUE demarrage et relit le lecteur attache par
|
||||
# l'hyperviseur, qui peut redefinir comptes, cles SSH, mots de passe et reseau. Sur
|
||||
# une machine que le plan possede desormais, c'est un second maitre — que le plan ne
|
||||
# decrit pas, et qui parle en premier.
|
||||
#
|
||||
# Il est retire ICI et pas ailleurs : apres que tout le reste soit pose, pour que la
|
||||
# machine ne depende plus de rien qu'il aurait fourni.
|
||||
- cloud_init_retrait
|
||||
|
|
|
|||
23
roles/cloud_init_retrait/README.md
Normal file
23
roles/cloud_init_retrait/README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# cloud_init_retrait
|
||||
|
||||
Retire `cloud-init` des machines **déployées**, dans le groupe `serveur_durci`.
|
||||
|
||||
**Pourquoi.** cloud-init est une source de vérité **externe** : à chaque démarrage il
|
||||
relit le lecteur cloud-init attaché par l'hyperviseur, qui peut redéfinir comptes, clés
|
||||
SSH, mots de passe et réseau. Sur une machine qu'Ansible possède, c'est un second maître
|
||||
que le plan ne décrit pas et que `make valider` ne mesure pas.
|
||||
|
||||
**Pourquoi c'est sans risque.** Mesuré le 2026-09-09 sur `obs-01` :
|
||||
`/etc/network/interfaces.d/50-cloud-init` n'appartient à aucun paquet (`dpkg -S` ne le
|
||||
trouve pas) et le `postrm` de `cloud-init` ne le nomme jamais, même en `purge`. L'adresse
|
||||
posée à la naissance survit. Le rôle le **vérifie** plutôt que de le supposer, et refuse
|
||||
d'aller plus loin si le fichier a disparu.
|
||||
|
||||
**Ce qui n'est pas retiré.** `cloud-guest-utils` (`growpart`) : aucun service, aucune
|
||||
source de données, aucun pouvoir. Le retirer ne fermerait rien.
|
||||
|
||||
**Le gabarit garde cloud-init** — il est le seul chemin vers la première seconde d'un
|
||||
clone (P56). Le retrait n'intervient qu'après, sur la machine déployée. C'est pourquoi
|
||||
`serveur_debian` ne l'applique plus : sinon le socle l'installerait et le durcissement le
|
||||
retirerait à chaque passage, un va-et-vient à chaque déploiement. **P63** garde les trois
|
||||
moitiés de cette décision.
|
||||
33
roles/cloud_init_retrait/defaults/main.yml
Normal file
33
roles/cloud_init_retrait/defaults/main.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
# `cloud-guest-utils` n'est PAS retire par defaut. Il ne porte ni service, ni source de
|
||||
# donnees, ni pouvoir d'ecrire sur la machine : c'est `growpart`, un utilitaire appele a
|
||||
# la main quand on agrandit un disque. Le retirer ne fermerait aucune surface — et le
|
||||
# jour ou l'on agrandit un disque, il faudrait le reinstaller.
|
||||
cloud_init_retrait_paquets:
|
||||
- cloud-init
|
||||
|
||||
# `purge` emporte `/etc/cloud`. C'est voulu : la configuration de cloud-init decrit QUI a
|
||||
# le droit de reconfigurer la machine, et on ne garde pas une declaration de pouvoir pour
|
||||
# un logiciel qu'on vient de retirer. Ce que le purge NE touche PAS est ce qui compte, et
|
||||
# c'est mesure : `/etc/network/interfaces.d/50-cloud-init` n'appartient a aucun paquet
|
||||
# (`dpkg -S` ne le trouve pas) et le `postrm` ne le nomme nulle part.
|
||||
cloud_init_retrait_purge: true
|
||||
|
||||
# Le fichier que cloud-init a ecrit a la naissance et qui porte l'adresse de la machine.
|
||||
# Nomme ici plutot qu'en dur : c'est LUI que le role verifie, et un chemin qu'on peut
|
||||
# lire est un chemin qu'on peut corriger.
|
||||
cloud_init_retrait_fichier_reseau: /etc/network/interfaces.d/50-cloud-init
|
||||
|
||||
# LES ORPHELINS, ET POURQUOI ON NE DECIDE PAS A LA PLACE DE L'OPERATEUR.
|
||||
# Retirer cloud-init laisse ~29 paquets qui n'etaient la que pour lui (mesure du
|
||||
# 2026-09-09 sur `obs-01` : python3-jsonschema, python3-jinja2, python3-oauthlib...).
|
||||
# Ce sont des BIBLIOTHEQUES : aucun service, aucun port, aucune source de donnees. Elles
|
||||
# pesent, elles n'ouvrent rien.
|
||||
#
|
||||
# `autoremove` calculerait exactement quoi retirer — mais il le calculerait a partir des
|
||||
# drapeaux « installe manuellement » de dpkg, et une machine dont ces drapeaux ont derive
|
||||
# y perdrait autre chose. Un durcissement ne doit pas pouvoir surprendre.
|
||||
#
|
||||
# Le defaut est donc `false` : on nomme ce qu'on retire. Passer a `true` est une decision
|
||||
# d'exploitation, prise en connaissance de cause.
|
||||
cloud_init_retrait_autoremove: false
|
||||
72
roles/cloud_init_retrait/tasks/main.yml
Normal file
72
roles/cloud_init_retrait/tasks/main.yml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
# CE QUE CE ROLE FERME. cloud-init n'est pas un logiciel d'installation : c'est une SOURCE
|
||||
# DE VERITE EXTERNE, qui se reveille a CHAQUE demarrage et relit le lecteur cloud-init
|
||||
# attache par l'hyperviseur. Ce lecteur peut redefinir les comptes, les cles SSH
|
||||
# autorisees, les mots de passe et le reseau. Sur une machine qu'Ansible possede
|
||||
# desormais, c'est un second maitre — que le plan ne decrit pas, que `make valider` ne
|
||||
# mesure pas, et qui gagne parce qu'il parle en premier.
|
||||
#
|
||||
# Sa tache est finie : il a donne a la VM son adresse, son nom et ses cles d'hote a la
|
||||
# premiere seconde. C'est precisement parce qu'il a REUSSI qu'Ansible a pu entrer.
|
||||
#
|
||||
# POURQUOI LE RETIRER NE COUPE PAS LE RESEAU (mesure du 2026-09-09 sur `obs-01`) :
|
||||
#
|
||||
# /etc/network/interfaces.d/50-cloud-init -> dpkg -S : aucun paquet ne le possede
|
||||
# /var/lib/dpkg/info/cloud-init.postrm -> ne nomme jamais interfaces.d
|
||||
#
|
||||
# Le fichier survit donc au retrait, `interfaces` fait toujours `source interfaces.d/*`,
|
||||
# et l'adresse derivee du plan reste posee. Son en-tete annonce que les modifications
|
||||
# « ne persistent pas au redemarrage » : c'etait vrai TANT QUE cloud-init pouvait le
|
||||
# reecrire. Une fois le paquet parti, plus personne ne le reecrit — l'avertissement
|
||||
# devient caduc, et le fichier devient la configuration.
|
||||
#
|
||||
# ON LE MESURE QUAND MEME. Une machine qui perd ce fichier ne se plaint pas : elle repart
|
||||
# au prochain demarrage sans adresse, et plus personne ne peut entrer pour le constater.
|
||||
# On releve donc son etat AVANT et APRES, et on n'accuse que si le retrait l'a emporte.
|
||||
|
||||
- name: Relever la configuration réseau AVANT le retrait
|
||||
ansible.builtin.stat:
|
||||
path: "{{ cloud_init_retrait_fichier_reseau }}"
|
||||
register: cloud_init_retrait_reseau_avant
|
||||
|
||||
- name: Retirer cloud-init (sa tâche est finie à la première seconde)
|
||||
ansible.builtin.apt:
|
||||
name: "{{ cloud_init_retrait_paquets }}"
|
||||
state: absent
|
||||
purge: "{{ cloud_init_retrait_purge | bool }}"
|
||||
autoremove: "{{ cloud_init_retrait_autoremove | bool }}"
|
||||
|
||||
- name: Relever la configuration réseau APRÈS le retrait
|
||||
ansible.builtin.stat:
|
||||
path: "{{ cloud_init_retrait_fichier_reseau }}"
|
||||
register: cloud_init_retrait_reseau_apres
|
||||
|
||||
# La faute exacte, et elle seule : le fichier etait la, il n'y est plus. Une machine qui
|
||||
# n'en a jamais eu (reseau tenu autrement) ne doit pas faire echouer le durcissement.
|
||||
- name: Refuser si le retrait a emporté la configuration réseau
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not (cloud_init_retrait_reseau_avant.stat.exists
|
||||
and not cloud_init_retrait_reseau_apres.stat.exists)
|
||||
fail_msg: >-
|
||||
{{ cloud_init_retrait_fichier_reseau }} a disparu avec cloud-init. Cette machine
|
||||
n'aurait plus d'adresse au prochain démarrage. NE PAS REDÉMARRER : restaurer le
|
||||
fichier d'abord (la configuration attendue se dérive du plan).
|
||||
success_msg: >-
|
||||
{{ 'Configuration réseau intacte après le retrait.'
|
||||
if cloud_init_retrait_reseau_apres.stat.exists
|
||||
else 'Cette machine ne tient pas son réseau par cloud-init — rien à préserver.' }}
|
||||
|
||||
# UNE REINSTALLATION PAR DEPENDANCE NE DOIT PAS RENDRE LE POUVOIR. `cloud-init` peut
|
||||
# revenir en recommandation d'un autre paquet ; ce drapeau est lu par cloud-init lui-meme
|
||||
# au demarrage et l'arrete avant qu'il ne lise la moindre source de donnees.
|
||||
- name: Poser le drapeau qui neutralise un cloud-init réinstallé
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/cloud/cloud-init.disabled
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
# Pose par le role cloud_init_retrait (groupe serveur_durci).
|
||||
# cloud-init a fait son travail a la premiere seconde de cette VM ; il n'a plus a
|
||||
# se reveiller. Si le paquet revient par dependance, ce fichier l'arrete.
|
||||
|
|
@ -2168,6 +2168,90 @@ def preuve_gabarit_minimal_et_repris() -> tuple[bool, str]:
|
|||
f"repris par le socle ou le durcissement.")
|
||||
|
||||
|
||||
def preuve_cloud_init_rendu_puis_retire() -> tuple[bool, str]:
|
||||
"""cloud-init nait avec la VM, et ne lui survit pas.
|
||||
|
||||
LE POUVOIR QU'IL GARDE (decision du 2026-09-09). cloud-init n'est pas un logiciel
|
||||
d'installation : c'est une SOURCE DE VERITE EXTERNE. A chaque demarrage il relit le
|
||||
lecteur attache par l'hyperviseur, qui peut redefinir comptes, cles SSH autorisees,
|
||||
mots de passe et reseau. Sur une machine que le plan possede, c'est un second maitre
|
||||
— que le plan ne decrit pas, que `make valider` ne mesure pas, et qui gagne parce
|
||||
qu'il parle en premier.
|
||||
|
||||
Sa tache est pourtant finie a la premiere seconde : c'est parce qu'il a REUSSI a poser
|
||||
l'adresse et les cles qu'Ansible a pu entrer.
|
||||
|
||||
TROIS MOITIES, ET ELLES SE DEFONT SEPAREMENT :
|
||||
|
||||
1. le GABARIT le garde. Sans lui, un clone n'a ni adresse ni nom : il ne nait pas.
|
||||
(P56 le tient deja comme indispensable ; on le redit ici parce que c'est la
|
||||
moitie qu'on serait tente de retirer « pour faire propre ».)
|
||||
2. le SOCLE ne l'installe plus. Le garder produisait un va-et-vient a chaque
|
||||
deploiement — le socle installe, le durcissement retire, deux `changed` par
|
||||
passage, l'idempotence perdue et `make valider` bruyant pour rien.
|
||||
3. le DURCISSEMENT le retire. C'est la moitie qui porte l'intention ; sans elle,
|
||||
les deux autres ne font que deplacer le probleme.
|
||||
|
||||
Une seule des trois qui bouge, et la decision devient son contraire en silence : un
|
||||
gabarit sans cloud-init donne des VM mortes ; un socle qui le reinstalle rend le
|
||||
pouvoir a chaque passage ; un durcissement qui ne le retire plus laisse le second
|
||||
maitre en place sans que rien ne le dise.
|
||||
"""
|
||||
def roles_de(chemin: str) -> list[str]:
|
||||
data = yaml.safe_load((RACINE / chemin).read_text(encoding="utf-8")) or []
|
||||
out: list[str] = []
|
||||
for play in data:
|
||||
for r in (play.get("roles") or []):
|
||||
nom = r if isinstance(r, str) else (r or {}).get("role")
|
||||
if nom:
|
||||
out.append(str(nom))
|
||||
return out
|
||||
|
||||
gabarit = roles_de("playbooks/modeles_vm/debian13_proxmox_preparer.yml")
|
||||
socle = roles_de("playbooks/groupes/serveur_debian.yml")
|
||||
durci = roles_de("playbooks/groupes/serveur_durci.yml")
|
||||
|
||||
fautes = []
|
||||
if "cloud_init" not in gabarit:
|
||||
fautes.append("le GABARIT ne porte plus `cloud_init` — un clone naitrait sans "
|
||||
"adresse ni nom d'hote, donc injoignable")
|
||||
if "cloud_init" in socle:
|
||||
fautes.append("le SOCLE applique encore `cloud_init` alors que le durcissement le "
|
||||
"retire — va-et-vient a chaque deploiement, idempotence perdue")
|
||||
if "cloud_init_retrait" not in durci:
|
||||
fautes.append("le DURCISSEMENT n'applique plus `cloud_init_retrait` — la source de "
|
||||
"verite externe reste en place, et rien ne le dit")
|
||||
|
||||
# Le role doit REELLEMENT retirer le paquet : une coquille vide passerait les trois
|
||||
# controles ci-dessus tout en ne fermant rien.
|
||||
defauts = RACINE / "roles" / "cloud_init_retrait" / "defaults" / "main.yml"
|
||||
taches = RACINE / "roles" / "cloud_init_retrait" / "tasks" / "main.yml"
|
||||
if not taches.is_file():
|
||||
fautes.append("`roles/cloud_init_retrait/tasks/main.yml` est absent")
|
||||
else:
|
||||
corps = yaml.safe_load(taches.read_text(encoding="utf-8")) or []
|
||||
retire = any((t.get("ansible.builtin.apt") or {}).get("state") == "absent"
|
||||
for t in corps if isinstance(t, dict))
|
||||
if not retire:
|
||||
fautes.append("`cloud_init_retrait` ne retire aucun paquet (`state: absent` "
|
||||
"absent) — le role existe mais ne ferme rien")
|
||||
garde = any("assert" in cle for t in corps if isinstance(t, dict) for cle in t)
|
||||
if not garde:
|
||||
fautes.append("`cloud_init_retrait` ne verifie plus que la configuration "
|
||||
"reseau survit au retrait — une VM sans adresse ne se plaint "
|
||||
"pas, elle disparait")
|
||||
if defauts.is_file():
|
||||
d = yaml.safe_load(defauts.read_text(encoding="utf-8")) or {}
|
||||
if "cloud-init" not in (d.get("cloud_init_retrait_paquets") or []):
|
||||
fautes.append("`cloud_init_retrait_paquets` ne nomme plus `cloud-init`")
|
||||
|
||||
if fautes:
|
||||
return False, "cloud-init :\n - " + "\n - ".join(fautes)
|
||||
return True, ("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 survit au retrait.")
|
||||
|
||||
|
||||
def preuve_cle_du_site_bornee_au_runner() -> tuple[bool, str]:
|
||||
"""La cle du SITE ne nait que sur le runner d'un tenant. Celle du tenant, partout chez lui.
|
||||
|
||||
|
|
@ -3011,6 +3095,8 @@ PREUVES: list[dict] = [
|
|||
"refs": ["AFF-033"], "func": preuve_schema_du_plan},
|
||||
{"id": "P62", "titre": "Schema du plan : il decrit tout ce que le MOTEUR accepte",
|
||||
"refs": ["AFF-033"], "func": preuve_schema_couvre_les_validateurs},
|
||||
{"id": "P63", "titre": "cloud-init nait avec la VM et ne lui survit pas", "refs": [],
|
||||
"func": preuve_cloud_init_rendu_puis_retire},
|
||||
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
||||
"func": preuve_devis_frontiere_du_site},
|
||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,22 @@ Set-OPS + Ansible ──> configuration réelle du serveur
|
|||
```
|
||||
C'est *pour ça* que l'infra est reconstructible : cloner + reconfigurer = quelques minutes.
|
||||
|
||||
**Et cloud-init s'efface ensuite.** Il ne s'arrête pas après la première seconde : il se
|
||||
réveille à *chaque* démarrage et relit le lecteur que l'hyperviseur a attaché à la VM — un
|
||||
lecteur qui peut redéfinir les comptes, les clés SSH autorisées, les mots de passe et le
|
||||
réseau. Sur une machine que le plan possède désormais, c'est un **second maître** : le plan
|
||||
ne le décrit pas, `make valider` ne le mesure pas, et il parle en premier.
|
||||
|
||||
Le durcissement (`serveur_durci`) le **retire** donc, une fois qu'il a réussi — et c'est
|
||||
parce qu'il a réussi qu'Ansible a pu entrer. Le gabarit, lui, le garde : sans lui, un clone
|
||||
n'a ni adresse ni nom d'hôte.
|
||||
|
||||
*Pourquoi ça ne coupe pas le réseau* : l'adresse posée à la naissance vit dans
|
||||
`/etc/network/interfaces.d/50-cloud-init`, un fichier qui **n'appartient à aucun paquet** —
|
||||
`dpkg -S` ne le trouve pas, et le `postrm` ne le nomme jamais, même en `purge` (mesuré le
|
||||
2026-09-09). Le rôle le vérifie quand même, 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 voir.
|
||||
|
||||
---
|
||||
|
||||
## ③ Pourquoi c'est transférable
|
||||
|
|
|
|||
Loading…
Reference in a new issue