Mise en conformité prouvable : registre d'affirmations + make prouver #1

Merged
danallaire merged 1 commit from conformite-prouvable into main 2026-07-20 20:01:48 -04:00
28 changed files with 1185 additions and 321 deletions

View file

@ -269,6 +269,8 @@ playbooks/
└── applications/ └── applications/
``` ```
À ce jour, seuls `groupes/`, `maintenance/`, `modeles_vm/` et `proxmox/` sont réellement peuplés. Les autres domaines de cette liste sont **prospectifs** : ils n'apparaissent que lorsqu'un besoin réel les justifie (cf. la règle « ne pas créer de structure inutile » ci-dessous).
Les rôles peuvent être ajoutés progressivement selon les besoins. Les rôles peuvent être ajoutés progressivement selon les besoins.
Ne pas créer de structure inutile uniquement pour donner une impression de complétude. Ne pas créer de structure inutile uniquement pour donner une impression de complétude.
@ -647,7 +649,10 @@ Ne pas créer de commit sans demande explicite.
--- ---
## Comportement attendu de Codex ## Comportement attendu des agents IA
Ce comportement s'applique à **tout agent IA** travaillant dans ce dépôt (Codex, Claude
Code, ou autre), conformément à la règle d'or « un seul agent IA à la fois ».
Avant de modifier : Avant de modifier :

View file

@ -1,5 +1,134 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-07-20
### Modifié
- **Conformité Phase 2 — 3 incohérences internes corrigées (documentation d'autorité).**
Aucun code Ansible touché ; le code SSH était déjà conforme à `AGENTS.md`.
- **`CLAUDE.md` réduit à un pointeur mince** : autorité unique d'`AGENTS.md` + les cinq
règles absolues (agent unique ; `hosts.yml` généré jamais édité ; aucun secret ;
confirmation des actions destructives ; rien n'est prêt sans validation). Toute la
doctrine dupliquée (template, SSH, pare-feu, cloud-init, handlers, Makefile…) retirée.
- **Contradiction SSH levée** : `CLAUDE.md` prescrivait `PasswordAuthentication yes`
pendant la construction ; le code applique en réalité `PasswordAuthentication no` +
`AuthenticationMethods publickey` dès le départ (prouvé, cf. `docs/audit/affirmations.md`
AFF-037/050). La doctrine SSH périmée de `CLAUDE.md` est supprimée — la duplication en
était la cause racine.
- **`AGENTS.md` : « Comportement attendu de Codex » → « …des agents IA »** (la section
vaut pour tout agent IA, Claude inclus).
- Suivi dans `docs/audit/affirmations.md` (§ Journal des traitements) : AFF-040, AFF-050,
AFF-052 résolus.
### Corrigé
- **Conformité Phase 3 — lot A « doc de démarrage » (le parcours QUICKSTART marche seul).**
- **Modèle absent (AFF-020/021).** `QUICKSTART.md` proposait `cp -r
exemples/modeles/presence-web …` — modèle **inexistant** dans le dépôt public (seul
`socle` l'est). Étape 2 réécrite sur `socle` ; les modèles assemblés renvoyés au dépôt
privé `Set-OPS-modeles`, cohérent avec `exemples/modeles/README.md`.
- **Chemin de voûte faux + piège de shadowing (AFF-023).** Le chemin `lab/` (QUICKSTART,
`exemples/vault.exemple.yml`, `docs/config-proxmox.md`) est corrigé en `production/`
(le modèle `socle` n'a qu'un inventaire `production/` ; `make config` résout
`lab > principal > production`). **Piège corrigé** : le socle livrait ses intrants en
`production/group_vars/all.yml` (forme fichier) ; y ajouter `all/vault.yml` (forme
dossier) fait **ignorer silencieusement** `all.yml` par Ansible (le dossier masque le
fichier — vérifié empiriquement). Le modèle est converti en forme dossier
(`group_vars/all/10-intrants.yml`), alignée sur l'instance prouvée. Validé :
`ansible-inventory --host` charge `domaine_interne` depuis la nouvelle disposition.
- **Commandes `make` périmées (AFF-080/081/082).** `make help``make` ;
`make syntax-template``make syntaxe-modele` (dans `docs/config-proxmox.md`,
`docs/modeles_vm/debian13-proxmox.md`, `docs/MISE-A-JOUR-CODEX-CLAUDE.md`).
- **Découvert et consigné (AFF-097, non traité ici) :** `lab/` codé en dur restant dans
les docs template/clone (`vm-lifecycle`, `procedure-template…`, `proxmox/README`,
message de `cloner_vm_debian.yml`) — lot séparé à prévoir.
- **Conformité Phase 3 — lot B « doc (prose) ».**
- **Prérequis Vault rappelé (AFF-026).** `QUICKSTART.md` : note que `make
inventaire-verifier` / `make verifier` chargent l'inventaire complet et exigent
`ANSIBLE_VAULT_PASSWORD_FILE`, sinon « no vault secrets found ».
- **Sous-dossiers `playbooks/` (AFF-039).** `AGENTS.md` : précisé que seuls `groupes/`,
`maintenance/`, `modeles_vm/`, `proxmox/` sont peuplés ; les autres sont prospectifs.
- **`SOLUTION.md` remis au présent (AFF-060/061).** Bannière « document historique »
(supplanté par README/QUICKSTART/`make`) ; arborescence complétée ; commande de
construction `--ask-pass` (mot de passe) → `make preparer-modele` (accès par clé).
- **Découvert et consigné (AFF-098, traitement B, non traité ici) :** contradiction
fonctionnelle — `make config` écrit le token Proxmox dans `all/vault.yml`, mais
`cloner_vm_debian.yml` ne le lit que depuis `proxmox.vault.yml`/env. Couplé à AFF-097
dans un futur lot « voûte Proxmox ».
### Corrigé
- **Conformité Phase 3 — lot « voûte Proxmox » (AFF-098 bug fonctionnel + AFF-097).**
- **Le clonage lit la voûte unifiée (AFF-098, option a).** `make config` écrit le token
Proxmox dans `instance/inventories/<env>/group_vars/all/vault.yml`, mais
`playbooks/proxmox/cloner_vm_debian.yml` (lancé `-i localhost,`) ne le chargeait que
depuis `proxmox.vault.yml``make creer-vm` échouait l'assert `proxmox_api_token_secret`
pour qui suivait la voûte unifiée. Corrigé : `all/vault.yml` ajouté aux sources de
secrets du playbook (autoritaire) ; la détection de voûte chiffrée du `Makefile`
(`cloner-vm`) cherche d'abord `all/vault.yml` puis `proxmox.vault.yml`. `proxmox.vault.yml`
reste accepté en compatibilité. **Validé** : `--syntax-check` OK, `ansible-lint` 0 échec,
test fonctionnel (token chargé depuis `all/vault.yml`, assert vert).
- **Docs Proxmox/template alignées (AFF-097).** `lab/` codé en dur → `production/` + voûte
unifiée dans `playbooks/proxmox/README.md`, `docs/procedure-template-debian13-proxmox.md`,
`docs/vm-lifecycle.md`, `docs/modeles_vm/debian13-proxmox.md`. Plus aucune référence
`inventories/lab/group_vars` dans les fichiers suivis.
- **Conformité Phase 3 — lot C « `make verifier` vert » (AFF-006).** `ansible-lint` passe de
**33 échecs à 0** (profil `min``production`), donc `make lint` **rc=0**. Trois causes :
- **`site.yml` généré lint-propre** : `scripts/orchestrer.py` émet un `name:` avant chaque
`import_playbook` (30× `name[play]`) ; `site.yml` régénéré. Orchestration inchangée.
- **`risky-shell-pipe`** : `set -o pipefail` + `executable: /bin/bash` sur les deux tâches
shell à pipe de `playbooks/valider.yml`.
- **`name[template]`** : Jinja déplacé en fin de `name` dans `supprimer_vm_debian.yml`.
**Validé** : `make lint` rc=0 ; toutes les étapes de `make verifier` vertes (lint, test,
site-verifier, flux-verifier, syntaxe) — seule `inventaire-verifier` requiert la voûte de
l'opérateur (prérequis documenté, AFF-026). Débloque AFF-002 (« exploitable sans IA » → ✅).
### Corrigé
- **Conformité Phase 5 — boucle documentaire (le parcours QUICKSTART marche seul, prouvé).**
Relecture de cohérence bout-en-bout (README/QUICKSTART/docs/wiki ↔ code final). Deux bogues
du modèle public/outillage débusqués et corrigés, en plus des alignements de prose :
- **Modèle `socle` invalide (AFF-099).** `exemples/modeles/socle/plan/domaines.yml` :
`autorite: interne` (périmé, rejeté par le validateur) → `auto-heberge`. Le modèle valide.
- **Split-brain d'inventaire (AFF-100).** `scripts/instancier.py` et `scripts/inventory_gui.py`
retombaient sur `principal/` quand aucun `hosts.yml` n'existe encore ; or le socle est en
`production/` → la 1ʳᵉ génération écrivait dans `principal/`, à côté des `group_vars` restés
en `production/`. Corrigé : le repli vise le **répertoire d'inventaire déjà présent** (comme
`config_proxmox.py`). Instances existantes (avec `hosts.yml`) **inchangées** (non-régression
vérifiée sur `principal`).
- **Alignements de prose.** `docs/intrants-communs.md` (`group_vars/all.yml` →
`all/10-intrants.yml`, forme dossier que le GUI écrit déjà) ; `QUICKSTART.md` étape 8
(`serveur_postgresql` → `serveur_powerdns`, groupe présent dans le socle).
- **Preuve** : parcours QUICKSTART rejoué **hors-ligne** sur une copie du socle
(`instancier generer`→`comparer`→`appliquer`) — écrit `production/hosts.yml`, diff **vide**
ensuite, 4 validateurs verts. Nouvelle preuve récurrente **P15** dans `make prouver`
(« modèle public socle valide ») : `make prouver` = **15 OK, 0 échec, 1 sautée**.
### Ajouté
- **Harnais de preuve `make prouver` (Phase 4).** Nouveau `scripts/prouver.py` — un
**orchestrateur mince** qui rejoue les preuves automatisables du registre en appelant
l'outillage **existant** (les mêmes scripts que `make verifier` : lint, tests, diff-vide,
validateurs de registres, cohérence groupes/playbooks, handlers, orchestration, flux,
syntaxe, existence des runbooks, invariants structurels) — **aucune validation
réimplémentée**. Produit `docs/audit/preuve-AAAA-MM-JJ.md` : rapport **horodaté,
rejouable**, reliant chaque preuve aux affirmations couvertes, listant à part les
déclarations d'intention (⚪). Sort en erreur si une preuve échoue ; la preuve `P15`
(inventaire Ansible complet) est **sautée** proprement sans mot de passe Vault (prérequis
AFF-026). Documenté dans `README.md` (une phrase) et `docs/audit/README.md` (mode
d'emploi complet + comment ajouter une preuve). `affirmations.md` : section « Couverture
par `make prouver` » reliant chaque ✅ à sa preuve. **1re exécution : 14 preuves OK,
0 échec, 1 sautée → CONFORME.**
- **Registre des affirmations (audit de conformité, Phase 1).** Nouveau
`docs/audit/affirmations.md` : chaque affirmation publique vérifiable du dépôt
(`README`, `AGENTS`, `CLAUDE`, `QUICKSTART`, `SOLUTION`, `docs/`, `wiki/`, aide du
`Makefile`, GUI) est tracée vers une **commande de preuve reproductible** et un statut
(✅ prouvée / 🟡 partielle / ❌ fausse / ⚪ invérifiable localement). **54 affirmations
enregistrées : 30 ✅, 13 🟡, 8 ❌, 3 ⚪.** Audit **sans aucun correctif** (les
traitements relèvent des phases suivantes). Preuves exécutées localement, hors
production : diff-vide du plan, recoupement notify↔handlers, validateurs de registres
(serveurs/applications/bases/domaines/GUI/orchestrateur/flux), `--syntax-check` de tous
les playbooks, test unitaire. Dix écarts majeurs classés par risque pour un opérateur
suivant la doc à la lettre — dont : `QUICKSTART` renvoie à un modèle absent
(`presence-web`), `make verifier` échoue (ansible-lint : 33 failures), contradiction
SSH `CLAUDE.md` ↔ code/`AGENTS.md`, chemin de voûte faux, commandes `make` périmées
dans `docs/`.
## 2026-07-07 (soir) ## 2026-07-07 (soir)
### Modifié ### Modifié

255
CLAUDE.md
View file

@ -1,244 +1,27 @@
# CLAUDE.md — Set-OPS # CLAUDE.md — Set-OPS
## Instruction principale ## Autorité
Claude Code doit lire et respecter `AGENTS.md` avant toute modification. `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.
`AGENTS.md` est la source dautorité principale du dépôt. En cas de contradiction entre `CLAUDE.md` et `AGENTS.md`, **suivre `AGENTS.md`**.
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
## Rôle du dépôt 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.
`Set-OPS` est le moteur Ansible global dexploitation décosystèmes numériques souverains. Le détail de chacune de ces règles, et tout le reste, est dans `AGENTS.md`.
Le template Debian 13 Proxmox est seulement un sous-ensemble du dépôt. Ne pas restructurer tout le dépôt autour de ce seul chantier.
---
## Règle dor IA
Un seul agent IA travaille dans ce dépôt à la fois.
- Soit Codex.
- Soit Claude Code.
- Jamais les deux simultanément.
---
## Avant toute modification
Exécuter ou demander léquivalent de :
```bash
git status --short
find . -maxdepth 3 -type f | sort
```
Lire au minimum :
```text
AGENTS.md
README.md
CHANGELOG.md
ansible.cfg
```
Lire aussi les fichiers directement concernés avant de les modifier.
Ne pas remplacer massivement larborescence sans demande explicite.
Préférer un correctif minimal ciblé.
---
## Validation Ansible obligatoire
Avant de dire quun changement est prêt, exécuter au minimum le `--syntax-check` du playbook touché.
Pour le template Debian 13 Proxmox :
```bash
ansible-playbook -i instance/inventories/lab/hosts.yml playbooks/modeles_vm/debian13_proxmox_preparer.yml --syntax-check
```
Si `ansible-lint` est disponible :
```bash
ansible-lint
```
Si `ansible-lint` nest pas disponible, le signaler clairement.
Ne jamais déclarer un playbook prêt si la validation échoue.
---
## Handlers Ansible
Chaque rôle qui utilise `notify` doit contenir son handler dans le rôle lui-même.
Exemple :
```text
roles/ssh_baseline/tasks/main.yml
roles/ssh_baseline/handlers/main.yml
```
Commandes utiles :
```bash
find roles -path '*/tasks/*.yml' -exec grep -H "notify:" {} \;
find roles -path '*/handlers/main.yml' -print
```
Avant de livrer un rôle, vérifier que chaque handler référencé existe réellement.
---
## Actions destructives
Toute action destructrice ou risquée doit exiger une confirmation explicite.
Exemple :
```yaml
confirm_destructive_action: true
```
Sont considérées risquées :
- modification bloquante de SSH ;
- activation ou modification dun pare-feu ;
- suppression dutilisateurs ;
- suppression de paquets critiques ;
- formatage disque ;
- modification de partitions ;
- redémarrage massif ;
- purge de données ;
- changement réseau pouvant couper laccès ;
- modification dun hyperviseur Proxmox ;
- opération sur stockage, iSCSI, ZFS ou Ceph.
Sans confirmation explicite, refuser lexécution.
---
## Template Debian 13 Proxmox
Le template doit rester un socle commun.
Il peut contenir :
- Debian minimal ;
- SSH ;
- sudo ;
- compte technique `ansible` ;
- sudo NOPASSWD pour `ansible` lorsque requis ;
- `qemu-guest-agent` ;
- `cloud-init` ;
- `cloud-guest-utils` ;
- chrony ;
- outils de diagnostic ;
- hardening raisonnable ;
- AppArmor ;
- auditd ;
- fail2ban SSH ;
- unattended-upgrades ;
- journald ;
- sysctl de sécurité ;
- nftables installé et préparé, mais non activé par défaut.
Il ne doit pas contenir par défaut :
- NGINX ;
- PostgreSQL ;
- MariaDB ;
- Docker ;
- Podman ;
- Redis ;
- GitLab ;
- Nextcloud ;
- monitoring complet ;
- données propres à un clone ;
- secrets ;
- clés privées.
---
## SSH
Pendant la construction du template :
```text
PasswordAuthentication yes
PermitRootLogin no
PubkeyAuthentication yes
```
État cible après validation des clés SSH :
```text
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
```
Ne jamais désactiver lauthentification par mot de passe avant davoir confirmé que laccès par clé fonctionne.
Toute modification SSH doit valider la configuration avant rechargement :
```bash
sshd -t
```
---
## Pare-feu
Ne pas activer un pare-feu générique dans le template sans confirmation explicite.
`nftables` peut être installé et préparé, mais rester désactivé dans le template.
Lactivation doit être faite sur un clone ou un serveur final, avec des règles adaptées au rôle du serveur.
---
## Cloud-init
Cloud-init donne lidentité initiale dun clone :
- hostname ;
- utilisateur initial ;
- clé SSH ;
- IP ;
- passerelle ;
- DNS ;
- agrandissement de la partition racine.
Cloud-init ne remplace pas Ansible.
Séparation attendue :
```text
Proxmox + cloud-init : identité initiale de la VM
Set-OPS + Ansible : configuration réelle du serveur
```
---
## Après modification
Répondre avec :
1. fichiers créés ;
2. fichiers modifiés ;
3. commandes de validation exécutées ;
4. résultat des validations ;
5. tests non exécutés ;
6. limites connues ;
7. entrée `CHANGELOG.md` ajoutée ou raison de labsence dentrée.
Ne pas dire que cest prêt si ce nest pas validé.

View file

@ -213,6 +213,12 @@ test:
.PHONY: verifier .PHONY: verifier
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe
# Harnais de preuve : rejoue les preuves automatisables du registre et ecrit
# docs/audit/preuve-<date>.md (piece justificative horodatee, rejouable).
.PHONY: prouver
prouver: ansible-runtime _instance-requise
python3 scripts/prouver.py
.PHONY: inventaire hote-planifier hote-ajouter hote-groupes hote-afficher appliquer deployer deployer-groupe cloner-vm creer-vm config inventaire-ui inventaire-verifier inventaire-lister inventaire-graphe inventaire-hote inventaire-lab inventaire-production instance-utiliser instance-courante .PHONY: inventaire hote-planifier hote-ajouter hote-groupes hote-afficher appliquer deployer deployer-groupe cloner-vm creer-vm config inventaire-ui inventaire-verifier inventaire-lister inventaire-graphe inventaire-hote inventaire-lab inventaire-production instance-utiliser instance-courante
inventaire: inventaire-production inventaire: inventaire-production
@ -507,8 +513,10 @@ cloner-vm: ansible-runtime
vault_args=(); \ vault_args=(); \
vault_file=""; \ vault_file=""; \
for d in lab principal production; do \ for d in lab principal production; do \
f="$(SETOPS_INSTANCE)/inventories/$$d/group_vars/proxmox.vault.yml"; \ for gv in group_vars/all/vault.yml group_vars/proxmox.vault.yml; do \
[[ -f "$$f" ]] && vault_file="$$f" && break; \ f="$(SETOPS_INSTANCE)/inventories/$$d/$$gv"; \
[[ -f "$$f" ]] && vault_file="$$f" && break 2; \
done; \
done; \ done; \
if [[ -n "$$vault_file" && -f "$$vault_file" ]]; then \ if [[ -n "$$vault_file" && -f "$$vault_file" ]]; then \
read -r premiere_ligne < "$$vault_file" || true; \ read -r premiere_ligne < "$$vault_file" || true; \

View file

@ -22,17 +22,20 @@ git clone <url-de-Set-OPS> Set-OPS && cd Set-OPS
``` ```
## 2. Choisir un modèle et créer ton instance ## 2. Choisir un modèle et créer ton instance
Les modèles sont dans `exemples/modeles/` (voir leur README). Choisis selon ton offre : Le dépôt public fournit **un modèle générique : `socle`** — le socle souverain minimal
`socle`, `presence-web`, `forge`, `identite`, `observabilite`, `integral`. (DNS interne, AC/PKI, edge TLS, relais courriel) sur lequel on ajoute des 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 ```bash
cp -r exemples/modeles/presence-web ../mon-instance # ton instance, ailleurs cp -r exemples/modeles/socle ../mon-instance # ton instance, ailleurs
ln -s ../mon-instance instance # le moteur la trouve via ce lien ln -s ../mon-instance instance # le moteur la trouve via ce lien
``` ```
*(Alternative au symlink : `export SETOPS_INSTANCE=../mon-instance`.)* *(Alternative au symlink : `export SETOPS_INSTANCE=../mon-instance`.)*
## 3. Renseigner ton instance (« tes couleurs ») ## 3. Renseigner ton instance (« tes couleurs »)
- `instance/inventories/production/group_vars/all.yml` → **`domaine_interne`** (ex. `monorg.internal`) ; - `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/nomenclature.yml` → ton **supernet** (ex. `10.20.0.0/16`) ;
- `instance/plan/domaines.yml` → ton **domaine public** ; - `instance/plan/domaines.yml` → ton **domaine public** ;
- `instance/plan/serveurs.yml`**placement Proxmox** (nœud, stockage, disque, mémoire, cœurs). - `instance/plan/serveurs.yml`**placement Proxmox** (nœud, stockage, disque, mémoire, cœurs).
@ -44,8 +47,8 @@ Tu peux aussi le faire dans le GUI plus tard (`make inventaire-ui`).
make config # renseigne API host/user/port, nœud, stockage, VMID du template... 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). 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 Tous tes secrets (token API + `vault_*`) vont dans **une voûte unique** :
environnement** : `instance/inventories/lab/group_vars/all/vault.yml`, à partir du `instance/inventories/production/group_vars/all/vault.yml`, à partir du
gabarit [`exemples/vault.exemple.yml`](exemples/vault.exemple.yml), chiffrée avec gabarit [`exemples/vault.exemple.yml`](exemples/vault.exemple.yml), chiffrée avec
`ansible-vault`. Exporte ton mot de passe Vault, ex. : `ansible-vault`. Exporte ton mot de passe Vault, ex. :
```bash ```bash
@ -86,7 +89,7 @@ Passe l'hôte en `etat: actif` dans `instance/plan/serveurs.yml`, régénère, d
make instancier-appliquer make instancier-appliquer
make deployer HOTE=infra-dns-01 # configure l'hôte selon ses groupes make deployer HOTE=infra-dns-01 # configure l'hôte selon ses groupes
# ou, par couche : # ou, par couche :
make deployer-groupe GROUPE=serveur_postgresql make deployer-groupe GROUPE=serveur_powerdns
``` ```
Les déploiements de groupe ne ciblent **que** les hôtes actifs. Les déploiements de groupe ne ciblent **que** les hôtes actifs.
@ -99,6 +102,9 @@ Les déploiements de groupe ne ciblent **que** les hôtes actifs.
make inventaire-verifier # registres + inventaire + garde-fous make inventaire-verifier # registres + inventaire + garde-fous
make verifier # + ansible-lint + --syntax-check make verifier # + ansible-lint + --syntax-check
``` ```
> Ces cibles chargent l'inventaire complet : exporte d'abord ton mot de passe Vault
> (étape 4, `ANSIBLE_VAULT_PASSWORD_FILE`), sinon Ansible s'arrête sur
> « Attempting to decrypt but no vault secrets found ».
--- ---

View file

@ -91,6 +91,13 @@ Valider le dépôt :
make verifier 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`) :
```bash
make prouver
```
Inspecter les inventaires : Inspecter les inventaires :
```bash ```bash

View file

@ -1,5 +1,11 @@
# La solution Set-OPS # 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 ## Décision
`Set-OPS` est le moteur Ansible global dexploitation décosystèmes numériques souverains. `Set-OPS` est le moteur Ansible global dexploitation décosystèmes numériques souverains.
@ -19,18 +25,24 @@ docs/
Les rôles sont nommés clairement, sans sous-arborescence complexe. 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 ## Commande principale pour construire le template
```bash Le template se construit avec un **accès SSH par clé dès le départ** (clé injectée par
ansible-playbook -i instance/inventories/lab/hosts.yml playbooks/modeles_vm/debian13_proxmox_preparer.yml --ask-pass --ask-become-pass cloud-init) — pas par mot de passe. La cible opérateur est :
```
Après que sudo sans mot de passe fonctionne :
```bash ```bash
ansible-playbook -i instance/inventories/lab/hosts.yml playbooks/modeles_vm/debian13_proxmox_preparer.yml 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 ## Vérification
```bash ```bash

View file

@ -143,7 +143,7 @@ find roles -path '*/handlers/main.yml' -print
Après correction : Après correction :
```bash ```bash
make syntax-template make syntaxe-modele
``` ```
Puis relancer : Puis relancer :

75
docs/audit/README.md Normal file
View file

@ -0,0 +1,75 @@
# Audit de conformité — mode d'emploi
Ce dossier contient le dispositif qui garde Set-OPS **honnête** : il ne doit jamais
affirmer plus que ce qu'il prouve.
## Les trois pièces
| Fichier | Rôle |
|---|---|
| [`affirmations.md`](affirmations.md) | **Le registre** : chaque affirmation publique du dépôt (README, AGENTS, QUICKSTART, docs, wiki, aide `make`, GUI) tracée vers une commande de preuve et un statut (✅/🟡/❌/⚪), plus le **journal des traitements** (ce qui a été corrigé, quand, comment). |
| `scripts/prouver.py` | **Le harnais** : un orchestrateur mince qui **rejoue** les preuves automatisables du registre en appelant l'outillage existant (les mêmes scripts que `make verifier`). Il ne réimplémente aucune validation. |
| `preuve-AAAA-MM-JJ.md` | **La pièce justificative** : le rapport horodaté produit par `make prouver`. Rejouable et présentable (audit, certification, revue). |
## Produire une preuve
```bash
make prouver
```
Cela exécute chaque preuve et écrit `docs/audit/preuve-<date>.md`. La commande **sort en
erreur (rc≠0)** si une preuve automatisable échoue — utilisable en garde-fou (CI locale,
pré-commit). Une preuve **SAUTÉE** (⚪) n'est pas un échec.
### Prérequis Vault
La preuve `P15` (inventaire Ansible complet, `ansible-inventory --list`) déchiffre le
`group_vars` de l'instance. Sans `ANSIBLE_VAULT_PASSWORD_FILE`, elle est **automatiquement
sautée** (⚪) avec la mention du prérequis — le reste du harnais reste vert, car les
validateurs Python lisent le plan et l'inventaire directement, sans secret. Pour l'inclure :
```bash
export ANSIBLE_VAULT_PASSWORD_FILE=~/.config/setops-vault-pass
make prouver
```
## Ce que couvre `make prouver`
| # | Preuve | Ce qu'elle établit |
|---|---|---|
| P01 | Lint (`ansible-lint`) | 0 violation, profil `production`. |
| P02 | Tests unitaires | `inventory_host` — cas nominal + refus. |
| P03 | Diff-vide du plan | l'inventaire est **généré** depuis le plan (diff vide). |
| P04 | Groupes ↔ playbooks | chaque groupe opérationnel a son playbook homonyme. |
| P05 | Dépendances de groupes | graphe cohérent, aucune entrée orpheline. |
| P06 | Validateurs de registres | serveurs / applications / bases / domaines valides. |
| P07 | GUI | `node --check` du JS du GUI. |
| P08 | Orchestration | couches + graphe : aucun cycle, aucune arête en arrière. |
| P09 | Flux réseau | schéma + matrice d'audit cohérents. |
| P10 | Handlers ↔ notify | tout `notify` pointe vers un handler du même rôle. |
| P11 | Syntaxe | `--syntax-check` de tous les playbooks (via `make syntaxe`). |
| P12 | Runbooks cités | les fichiers `docs/` référencés existent. |
| P13 | Invariants structurels | LICENSE, socle en forme dossier, pas de couches parallèles, SSH clé-only, nftables désactivé par défaut. |
| P14 | Chemins d'inventaire | aucun `instance/inventories/lab/group_vars` codé en dur. |
| P15 | Modèle public `socle` | ses registres (domaines/serveurs/applications/bases) valident. |
| P16 | Inventaire Ansible (voûte) | `ansible-inventory --list` — sauté sans mot de passe Vault. |
Le rapport relie chaque preuve aux **affirmations** qu'elle couvre (colonne « Affirmations »),
et liste à part les **déclarations d'intention** (⚪ invérifiables localement : AFF-036,
091, 096, 007) — assumées comme intentions, jamais présentées comme prouvées.
## Ajouter une preuve
1. Ajouter (ou corriger) l'affirmation dans `affirmations.md` avec sa commande de preuve.
2. Ajouter une entrée à la liste `PREUVES` de `scripts/prouver.py` : soit une ou plusieurs
commandes (`cmds`, toutes doivent renvoyer 0), soit une fonction native `func` renvoyant
`(ok, détail)` pour un invariant simple. Renseigner `refs` avec les ID d'affirmations.
3. Ne **pas** réimplémenter de logique de validation : appeler l'outillage existant
(`scripts/*.py`, `ansible-lint`, cibles `make`). Le harnais orchestre, il ne valide pas.
## Rapport avec `make verifier`
`make verifier` est la **porte de validation** au fil de l'eau (lint, tests, cohérence,
syntaxe) et s'arrête au premier échec. `make prouver` est complémentaire : il **exécute
tout** (sans s'arrêter au premier échec), **horodate** et **relie** chaque résultat aux
affirmations — pour produire une pièce présentable. Les deux réutilisent le même outillage.

390
docs/audit/affirmations.md Normal file
View file

@ -0,0 +1,390 @@
# Registre des affirmations — Set-OPS
> **Phase 1 de la mise en conformité prouvable.** Ce document est un **audit**, sans
> aucun correctif de code ni de documentation. Chaque affirmation publique vérifiable
> du dépôt est tracée vers une commande de preuve reproductible, ou marquée comme
> invérifiable.
>
> Règle d'arbitrage (rappel) : *le dépôt ne doit jamais affirmer plus que ce qu'il
> prouve.* Les traitements (corriger la doc / corriger le code / chantier séparé)
> relèvent de la **Phase 3** — ils ne sont **pas** appliqués ici.
- **Date de l'audit** : 2026-07-20
- **Instance montée pendant l'audit** : `instance -> ../OPS-Technolibre` (inventaire `principal/`)
- **Outillage** : ansible-core 2.18.1, ansible-lint 25.1.3, Python 3.13.3, node présent.
- **Périmètre** : surfaces publiques uniquement — `README.md`, `AGENTS.md`,
`CLAUDE.md`, `QUICKSTART.md`, `SOLUTION.md`, `docs/`, `wiki/`, aide du `Makefile`,
libellés du GUI. Aucune VM / Proxmox / réseau touché (mission hors production).
Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invérifiable localement.
---
## 1. README.md
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-001 | « Set-OPS est un moteur Ansible générique … à partir d'un plan déclaratif » (README:3) | fonctionnelle | `python3 scripts/instancier.py generer && python3 scripts/instancier.py comparer` | ✅ | « DIFF VIDE : le plan reproduit exactement l'inventaire ». Générateur opérationnel. |
| AFF-002 | « Set-OPS s'exploite entièrement à la main — la doc, `make` et le GUI suffisent, sans aucune IA » (README:9 ; AGENTS principe 10) | doctrinale | Suivre `QUICKSTART.md` de bout en bout ; `make verifier` | 🟡 | Le principe tient au niveau outillage, mais **deux trous cassent le parcours sans IA** : `make verifier` échoue (AFF-006) et `QUICKSTART` étape 2 renvoie à un modèle absent (AFF-020/021). Un opérateur seul bute. |
| AFF-003 | Registres = source unique de vérité : `serveurs.yml`, `applications.yml`, `bases-donnees.yml`, `domaines.yml`, `nomenclature.yml` (README:15) | structurelle | `make inventaire-verifier` (serveurs/applications/bases/domaines) | ✅ | Les 4 validateurs renvoient « Registre valide ». Fichiers présents dans `instance/plan/`. |
| AFF-004 | « `instance/inventories/production/hosts.yml` est généré depuis le plan — ne pas l'éditer à la main » (README:34) | procédurale | `python3 scripts/instancier.py comparer` (diff-vide) | ✅ | Diff vide confirmé. (Nuance de chemin : l'instance courante utilise `principal/` — voir AFF-023.) |
| AFF-005 | « Afficher l'aide : `make` » (README:86) | procédurale | `make` (défaut = cible `aide`) | ✅ | `.DEFAULT_GOAL := aide` ; l'aide s'affiche. |
| AFF-006 | « Valider le dépôt : `make verifier` » (README:91) | fonctionnelle | `make verifier` | ❌ | `make lint` (1re dépendance de `verifier`) **échoue rc=2** : ansible-lint rapporte « Failed: 33 failure(s) » (`name[play]` sur le `site.yml` généré, `risky-shell-pipe` dans `site.yml`/`valider.yml`, `name[template]`). `make verifier` ne passe donc **pas** sur un dépôt propre. |
| 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-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. |
---
## 2. QUICKSTART.md
| 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). |
| AFF-021 | « `cp -r exemples/modeles/presence-web ../mon-instance` » (QUICKSTART:29) | procédurale | exécuter la commande | ❌ | La commande **échoue** : `exemples/modeles/presence-web` n'existe pas. C'est la commande centrale de l'étape 2 ; un opérateur suivant QUICKSTART à la lettre est bloqué immédiatement. Seule valeur valide : `socle`. |
| AFF-022 | « `instance/inventories/production/group_vars/all.yml``domaine_interne` » (QUICKSTART:36) | structurelle | `test -f exemples/modeles/socle/inventories/production/group_vars/all.yml` | ✅ | Le modèle `socle` fournit bien ce fichier sous `production/`. |
| AFF-023 | « Tous tes secrets … `instance/inventories/lab/group_vars/all/vault.yml` » (QUICKSTART:48-49) | procédurale | `ls exemples/modeles/socle/inventories/` | ❌ | Le modèle `socle` n'a **pas** d'inventaire `lab/` (uniquement `production/`). Le chemin de voûte annoncé n'existe pas dans le modèle public ; incohérent avec AFF-022 (`production/`). |
| AFF-024 | « `make instancier` (diff) puis `make instancier-appliquer FORCE=1` — 1re génération » (QUICKSTART:68-70) | fonctionnelle | `make instancier` ; cible `instancier-appliquer` (Makefile:582) | ✅ | Cibles présentes ; `FORCE` mappé sur `--force`. Diff-vide prouve le mécanisme (AFF-001). |
| AFF-025 | Étape 5 : `make preparer-modele` / `verifier-modele` / `nettoyer-modele CONFIRMER=true` (QUICKSTART:59-62) | procédurale | cibles Makefile:632-643 | 🟡 | Cibles présentes et gardes vérifiées ; l'exécution réelle exige une VM Debian 13 + Proxmox → hors production, non testable ici. |
| AFF-026 | « Valider à tout moment : `make inventaire-verifier` / `make verifier` » (QUICKSTART:98-101) | fonctionnelle | `make inventaire-verifier` ; `make verifier` | 🟡 | `make verifier` échoue (AFF-006). `make inventaire-verifier` appelle `ansible-inventory --list` qui **exige le mot de passe Vault** (`ERROR! Attempting to decrypt but no vault secrets found` sans `ANSIBLE_VAULT_PASSWORD_FILE`) — prérequis non rappelé à cet endroit. |
---
## 3. AGENTS.md
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-030 | « RÈGLE D'OR : `…/production/hosts.yml` est un artefact GÉNÉRÉ » (AGENTS:43) | procédurale | `python3 scripts/instancier.py comparer` | ✅ | Diff vide : l'inventaire est reproductible depuis le plan. |
| AFF-031 | « `make instancier-appliquer` refuse si le diff n'est pas vide (`FORCE=1` sinon) » (AGENTS:47) | fonctionnelle | lecture `scripts/instancier.py appliquer` + Makefile:582-583 | ✅ | `appliquer` accepte `--force` (mappé `FORCE`) ; garde diff cohérente avec le comparateur. |
| AFF-032 | « VMID / IP / VLAN / passerelle sont dérivés de la `fonction` via la nomenclature » (AGENTS:49) | fonctionnelle | `python3 scripts/serveurs.py verifier` (réconcilie dérivation ↔ inventaire) | ✅ | « Registre des serveurs valide et réconcilié avec l'inventaire. » Dérivation confirmée. |
| AFF-033 | « garde-fous : … `node --check` du JS du GUI (`scripts/verifier_gui.py`, dans `make inventaire-verifier`) » (AGENTS:50) | fonctionnelle | `python3 scripts/verifier_gui.py` | ✅ | « JS du GUI : syntaxe valide (node --check). » |
| AFF-034 | « Chaque rôle qui utilise `notify` doit contenir son handler dans le rôle » (AGENTS:182) | structurelle | script de recoupement notify↔handlers sur tous les rôles | ✅ | « OK : tout `notify` pointe vers un handler existant dans le même rôle. » |
| AFF-035 | « Tout `notify` doit pointer vers un handler existant » (AGENTS:193) | structurelle | idem AFF-034 | ✅ | Aucun `notify` orphelin. |
| AFF-036 | « Playbooks … testables avec `--check` autant que possible » (AGENTS:214, 427) | fonctionnelle | `ansible-playbook … --check` sur cibles vivantes | ⚪ | Non testable hors flotte réelle (mission hors production). Recensement des rôles non-check reporté (nécessite des hôtes). |
| AFF-037 | État SSH attendu : `PasswordAuthentication no`, `AuthenticationMethods publickey` (AGENTS:538-542) | fonctionnelle | lecture `roles/ssh_baseline/defaults` + `templates/10-setops.conf.j2` | ✅ | Défauts = `no`/`no`/`yes` ; le template code `AuthenticationMethods publickey`. **Le code suit AGENTS.md.** (Contredit CLAUDE.md — voir AFF-050.) |
| AFF-038 | « Ne pas maintenir en parallèle `playbooks/socle/` ou `playbooks/durcissement/` » (AGENTS:278) | structurelle | `ls playbooks/` | ✅ | Ces dossiers n'existent pas ; socle/durcissement = groupes `serveur_debian`/`serveur_durci`. |
| AFF-039 | « Les playbooks peuvent être classés par domaine : groupes/ maintenance/ monitoring/ networking/ proxmox/ modeles_vm/ web/ database/ identity/ backup/ applications/ » (AGENTS:257-270) | structurelle | `find playbooks -maxdepth 1 -type d` | 🟡 | Seuls `groupes/`, `maintenance/`, `modeles_vm/`, `proxmox/` existent. Formulé au conditionnel (« peuvent ») → aspirationnel, pas une fausse affirmation, mais liste plus large que la réalité. |
| AFF-040 | Section « Comportement attendu de **Codex** » (AGENTS:650) | doctrinale | lecture | 🟡 | La section ne nomme que Codex alors qu'elle vaut pour tout agent (Claude inclus). Correctif de titrage prévu en Phase 2 (item 3). |
---
## 4. CLAUDE.md
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-050 | « Pendant la construction du template : `PasswordAuthentication yes` » (CLAUDE.md § SSH) | fonctionnelle | `roles/ssh_baseline/defaults/main.yml` | ❌ | Le code déploie `PasswordAuthentication no` **dès la construction** (`ssh_baseline_password_authentication: "no"`, template `AuthenticationMethods publickey`). CLAUDE.md contredit **et** le code **et** AGENTS.md (qui fait autorité). Cause racine : duplication de doctrine. Cible Phase 2 (item 1). |
| AFF-051 | « Claude Code doit lire et respecter `AGENTS.md` avant toute modification ; en cas de contradiction, suivre `AGENTS.md` » (CLAUDE.md § Instruction principale) | doctrinale | — | ✅ | Cohérent avec AGENTS.md ; pas de commande de preuve (déclaration de gouvernance). |
| AFF-052 | CLAUDE.md redéclare la doctrine de template, SSH, pare-feu, cloud-init, handlers, actions destructives (dupliquée d'AGENTS.md) | structurelle | `diff` conceptuel CLAUDE.md ↔ AGENTS.md | 🟡 | Large duplication → source des dérives (AFF-050). Réduction à un pointeur mince prévue Phase 2 (item 2). |
---
## 5. SOLUTION.md
| 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). |
| AFF-061 | Commande template : `ansible-playbook … debian13_proxmox_preparer.yml --ask-pass --ask-become-pass` (SOLUTION:24-25) | procédurale | comparaison avec la doctrine SSH clé-only | 🟡 | `--ask-pass` (mot de passe SSH) contredit l'accès **par clé dès le départ** d'AGENTS.md (AFF-037). SOLUTION.md décrit un flux périmé (mot de passe SSH) ; le flux réel = clé via cloud-init. |
| AFF-062 | « Le pare-feu `nftables` est installé et préparé, mais pas activé par défaut dans le template » (SOLUTION:60 ; AGENTS:559 ; CLAUDE.md) | fonctionnelle | `grep enabled roles/nftables_baseline/defaults/main.yml` | ✅ | `nftables_baseline_enabled: false`. `preparer-modele` inclut le rôle et affiche « nftables est installé mais désactivé par défaut ». |
---
## 6. Aide du Makefile (`make`)
| 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-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é. |
| AFF-075 | Cibles dépréciées `make hote-planifier / hote-ajouter / hote-groupes` renvoient vers le plan (README:118 ; Makefile:237) | procédurale | `make hote-planifier` | ✅ | Stub présent : message « Cible dépréciée … » + `exit 2`. |
---
## 7. docs/ — runbooks et cohérence des commandes
| 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. |
| 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é. |
| AFF-082 | « `make syntax-template` » (docs/MISE-A-JOUR-CODEX-CLAUDE.md:146) | procédurale | idem | ❌ | Même nom périmé, second emplacement. |
| 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-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). |
| 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. |
| 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 ». |
---
## 8. wiki/
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-090 | Le wiki pédagogique fournit des unités d'apprentissage par service (`wiki/*.md`) publiables dans Forgejo | structurelle | `ls wiki/*.md` | ✅ | 16 pages de contenu (+ `README.md` + `_Sidebar.md`) ; 17 publiées par `wiki-publier` (README exclu). |
| AFF-091 | Contenu conceptuel du wiki (OIDC, PKI, DNS, sauvegardes, idempotence, RBAC…) | doctrinale | — | ⚪ | Contenu pédagogique = affirmations conceptuelles, non rattachées à une preuve exécutable du dépôt. Hors périmètre de preuve automatisable. |
---
## 9. GUI (libellés)
| ID | Affirmation (source) | Type | Commande de preuve | Statut | Écart constaté |
|---|---|---|---|---|---|
| AFF-095 | « La vue **Inventaire** est en lecture seule ; vues éditables **Serveurs**, **Applications**, **Domaines** ; vues lecture seule **Réseau**, **Flux**, **Couches** » (README:41 ; AGENTS:48 ; CHANGELOG) | fonctionnelle | démarrage headless de `scripts/inventory_gui.py` + inspection des routes/onglets | 🟡 | Les routes backend et l'import des validateurs sont présents dans `scripts/inventory_gui.py` ; le rendu des onglets et le caractère « lecture seule » ne sont pas vérifiés sans lancer le serveur + un navigateur (non fait — pas de commande de preuve headless au registre). |
| AFF-096 | « GUI 100 % française » (CHANGELOG 2026-07-03) | structurelle | inspection des chaînes du GUI | ⚪ | Vérifiable seulement par revue exhaustive des libellés rendus ; non automatisé ici. |
---
## Décompte
Recompte exact par ID (numérotation à trous AFF-001 → AFF-096, **54 affirmations**).
| Statut | Nombre |
|---|---|
| ✅ prouvée | 30 |
| 🟡 partielle | 13 |
| ❌ fausse | 8 |
| ⚪ invérifiable localement | 3 |
| **Total** | **54** |
Détail :
- ✅ (30) : AFF-001, 003, 004, 005, 008, 009, 010, 011, 012, 014, 015, 022, 024, 030, 031, 032, 033, 034, 035, 037, 038, 051, 062, 070, 071, 073, 075, 083, 084, 090.
- 🟡 (13) : AFF-002, 007, 013, 025, 026, 039, 040, 052, 060, 061, 072, 074, 095.
- ❌ (8) : AFF-006, 020, 021, 023, 050, 080, 081, 082.
- ⚪ (3) : AFF-036, 091, 096.
> La Phase 4 (`make prouver`) rattachera chaque ✅ à sa section de preuve et figera le
> compte.
---
## Les dix écarts les plus graves
Classés par risque pour un opérateur qui suit la doc **à la lettre, sans IA**.
1. **AFF-021 / AFF-020 — QUICKSTART renvoie à un modèle inexistant.**
`cp -r exemples/modeles/presence-web ../mon-instance` échoue : seul `socle` est
public. **L'opérateur est bloqué dès l'étape 2** du guide de démarrage. Risque
maximal : c'est la toute première action concrète du parcours « zéro → écosystème ».
2. **AFF-006 — `make verifier` échoue sur un dépôt propre.**
`ansible-lint` rapporte 33 échecs (surtout `name[play]` sur le `site.yml` **généré**
par l'outil lui-même, + `risky-shell-pipe`). La commande de validation vantée par
README et QUICKSTART ne passe pas → l'opérateur croit son dépôt cassé, ou apprend à
ignorer l'échec (perte de confiance dans le harnais).
3. **AFF-050 — Contradiction SSH doc vs code (sécurité).**
`CLAUDE.md` prescrit `PasswordAuthentication yes` pendant la construction ; le code
et `AGENTS.md` imposent `no` + `AuthenticationMethods publickey`. Un opérateur qui
suit CLAUDE.md attend un accès mot de passe qui n'existe pas — et la doctrine de
sécurité affichée est contradictoire.
4. **AFF-023 — Chemin de voûte faux dans QUICKSTART.**
L'étape 4 place les secrets dans `inventories/lab/group_vars/all/vault.yml`, mais le
modèle public `socle` n'a pas d'inventaire `lab/` (seulement `production/`).
L'opérateur crée sa voûte au mauvais endroit → déchiffrement introuvable au déploiement.
5. **AFF-026 — `make inventaire-verifier` exige le Vault sans le dire.**
La validation « à tout moment » appelle `ansible-inventory --list` qui échoue
(`no vault secrets found`) sans `ANSIBLE_VAULT_PASSWORD_FILE`. Prérequis non rappelé
au point d'usage → échec surprenant pour qui n'a pas exporté sa clé.
6. **AFF-002 — Promesse « exploitable sans IA » entamée par 15.**
Le principe fondateur (AGENTS principe 10) est **partiellement démenti par les faits** :
le parcours documenté comporte au moins deux ruptures dures (modèle absent, `make
verifier` cassé). C'est le méta-risque : la doc ne mène pas seule au succès.
7. **AFF-080 / AFF-081 / AFF-082 — Commandes `make` périmées dans la doc.**
`make help` (→ `aide`) et `make syntax-template` (→ `syntaxe-modele`, cité 2×)
n'existent pas. L'opérateur tape une commande qui renvoie une erreur `make`.
8. **AFF-061 — SOLUTION.md décrit un flux SSH périmé.**
`--ask-pass --ask-become-pass` suppose un accès par mot de passe contraire au modèle
clé-only réel. Suivre SOLUTION.md pour construire le template mène à un flux qui ne
correspond plus au code.
9. **AFF-060 — SOLUTION.md décrit une arborescence obsolète.**
« Une seule structure : inventories/playbooks/roles/docs/ » ignore `scripts/`,
`instance/`, `filter_plugins/`, `wiki/`, `exemples/` — désoriente un nouveau lecteur
sur l'endroit où vivent le plan et le GUI.
10. **AFF-052 / AFF-040 — Doctrine dupliquée et titrage « Codex ».**
La duplication CLAUDE.md ↔ AGENTS.md est la **cause racine** de la contradiction n°3 ;
la section « Comportement attendu de Codex » sous-entend à tort qu'elle ne vise pas
Claude. Risque de gouvernance : deux sources d'autorité qui divergent avec le temps.
---
## Journal des traitements
Le tableau ci-dessus est le **snapshot d'audit Phase 1 (2026-07-20)** et n'est pas
réécrit. Les traitements appliqués ensuite sont consignés ici, datés et traçables.
### Phase 2 — 2026-07-20 (3 incohérences internes pré-cadrées)
- **AFF-050 (❌ → résolu)** — Doctrine SSH contradictoire. Vérifié que le code applique
déjà `PasswordAuthentication no` + `AuthenticationMethods publickey` dès la construction
(aligné sur `AGENTS.md`, cf. AFF-037). **Aucun correctif de code** ; la doctrine SSH a
été **retirée de `CLAUDE.md`**.
- **AFF-052 (🟡 → résolu)** — Duplication CLAUDE.md ↔ AGENTS.md. `CLAUDE.md` réduit à un
pointeur mince (autorité d'`AGENTS.md` + 5 règles absolues) ; cause racine d'AFF-050 supprimée.
- **AFF-040 (🟡 → résolu)** — Section « Comportement attendu de **Codex** » renommée
« …des agents IA » dans `AGENTS.md`, avec mention explicite qu'elle vaut pour tout agent.
Reste ouvert après Phase 2 : les 5 ❌ restants (AFF-006, 020, 021, 023, 080/081/082) et
les 🟡 relèvent de la **Phase 3 (arbitrage)**.
### Phase 3 — Lot A « doc de démarrage » — 2026-07-20
- **AFF-020/021 (❌ → résolu, A)**`QUICKSTART.md` étape 2 : ne propose plus que le
modèle public `socle` (`cp -r exemples/modeles/socle …`) ; les modèles assemblés
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
`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`).
- **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).
### Phase 3 — Lot B « doc (prose) » — 2026-07-20
- **AFF-026 (🟡 → résolu, A)**`QUICKSTART.md` § « Valider à tout moment » : note ajoutée
que `inventaire-verifier`/`verifier` chargent l'inventaire complet et exigent
`ANSIBLE_VAULT_PASSWORD_FILE` (étape 4), sinon « no vault secrets found ».
- **AFF-039 (🟡 → résolu, A)**`AGENTS.md` : note ajoutée sous la liste des sous-dossiers
`playbooks/` — seuls `groupes/`, `maintenance/`, `modeles_vm/`, `proxmox/` sont peuplés ;
les autres sont **prospectifs**.
- **AFF-060/061 (🟡 → résolu, A)**`SOLUTION.md` : bannière « document historique » ajoutée
(supplanté par README/QUICKSTART/`make`) ; arborescence complétée (`scripts/`, `wiki/`,
`instance/`…) ; commande de construction `--ask-pass` (mot de passe) remplacée par
`make preparer-modele` (accès par clé, aligné `AGENTS.md`).
- **AFF-002 (🟡)** — « exploitable sans IA » : **pas de reformulation** (c'est un engagement
fondateur légitime, pas une sur-affirmation). Son seul écart résiduel est AFF-006
(`make verifier` casse) → bascule ✅ une fois AFF-006 traité (lot C, B). Aucune édition.
- **AFF-007 (🟡)**`web-frontal-01` : **assumé** comme nom d'exemple/placeholder (usage
cohérent dans tout le README) ; aucune édition.
- **AFF-097 / AFF-098****différés** au lot « voûte Proxmox » : AFF-098 (contradiction
fonctionnelle `all/vault.yml` ↔ clonage, traitement B) est couplé à AFF-097 (mêmes
fichiers) ; les traiter ensemble évite un double passage.
### Phase 3 — Lot « voûte Proxmox » — 2026-07-20
- **AFF-098 (❌ → résolu, B, option a)** — Le clonage lit désormais la voûte **unifiée**.
`playbooks/proxmox/cloner_vm_debian.yml` : `all/vault.yml` ajouté à la liste des sources
de secrets Proxmox (chargé via le filtre `is search('vault')`, autoritaire car en
dernier). `Makefile` (`cloner-vm`) : la détection de voûte chiffrée cherche d'abord
`group_vars/all/vault.yml` puis `proxmox.vault.yml` (déclenche `--ask-vault-pass`).
Messages de tâche/rescousse/assert rendus génériques. **Validé** : `--syntax-check` OK,
`ansible-lint` 0 échec sur le playbook, et **test fonctionnel** (mini-instance en clair) —
le token est bien chargé depuis `all/vault.yml` et l'hôte depuis `proxmox.yml`
(assert vert). `proxmox.vault.yml` reste accepté en compatibilité.
- **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
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).
### Phase 3 — Lot C « make verifier vert » — 2026-07-20
- **AFF-006 (❌ → résolu, B)**`ansible-lint` passe de **33 échecs à 0** (profil monté
`min``production`), donc `make lint` **rc=0**. Trois causes traitées :
- **30× `name[play]`** sur le `site.yml` **généré**`scripts/orchestrer.py` (`rendre_site`)
émet désormais `- name: Couche <c> — groupe <g>` avant chaque `import_playbook` ;
`site.yml` régénéré (`make site`). Logique d'orchestration inchangée (`orchestrer verifier`
OK, 30 groupes, syntax-check OK).
- **2× `risky-shell-pipe`** (`playbooks/valider.yml`) → `set -o pipefail;` +
`executable: /bin/bash` sur les deux tâches shell à pipe.
- **1× `name[template]`** (`playbooks/proxmox/supprimer_vm_debian.yml`) → Jinja déplacé en
fin de `name` (`… (VMID {{ … }})`).
- **Validé** : `make lint` rc=0 ; `make verifier` franchit lint + test (4) + `site-verifier`
+ `flux-verifier` + `syntaxe` (35 playbooks) — **toutes vertes**. Seule `inventaire-verifier`
exige la voûte de l'opérateur (`ANSIBLE_VAULT_PASSWORD_FILE`), prérequis documenté (AFF-026).
- **AFF-002 (🟡 → ✅)** — « exploitable sans IA » : ses deux écarts résiduels sont soldés
(parcours QUICKSTART, lot A ; `make verifier`/lint, ce lot). Bascule ✅, sous le prérequis
Vault désormais rappelé (AFF-026).
### Phase 5 — Boucle documentaire — 2026-07-20
Relecture de cohérence bout-en-bout (README, QUICKSTART, docs, wiki) contre le code final,
au regard du critère du principe 10 : *un opérateur sans IA exécute chaque procédure sans
commande morte, chemin faux ni étape manquante.*
- **Sweep commandes/chemins** : toutes les commandes `make` citées existent ; jetons périmés
(`presence-web` hors contexte, `syntax-template`, `make help`, `--ask-pass`, voûte `lab/`)
soldés (les occurrences restantes ne sont que dans `CHANGELOG.md`/`docs/audit`, historiques).
- **AFF-099 (❌ → résolu, B)**`exemples/modeles/socle/plan/domaines.yml` : `interne`
`auto-heberge`. Le modèle public valide désormais.
- **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`
`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 —
`instancier generer`→`comparer`→`appliquer --force` **écrit `production/hosts.yml`**, diff
**vide** ensuite (idempotence), et les 4 validateurs verts (`serveurs` **réconcilié**).
Non-régression vérifiée sur l'instance réelle `principal` (diff vide, cible inchangée).
## Couverture par `make prouver` (Phase 4)
Chaque affirmation ✅ est soit **rejouée** par une preuve de `make prouver`
(`scripts/prouver.py` → `docs/audit/preuve-<date>.md`), soit **structurelle/doctrinale**
(vérifiée à l'audit, rien d'exécutable à rejouer en boucle).
| Affirmation ✅ | Preuve `make prouver` |
|---|---|
| AFF-001, 004, 030, 031, 032 | **P03** (diff-vide) |
| AFF-003 | **P06** (validateurs de registres) |
| AFF-008 | **P04** (groupes ↔ playbooks) |
| AFF-009, 084 | **P05** (dépendances) |
| AFF-010, 011, 012 | **P12** (existence des runbooks) |
| AFF-015, 022, 037, 038, 062 | **P13** (invariants structurels) |
| AFF-033 | **P07** (GUI node --check) |
| AFF-034, 035 | **P10** (handlers ↔ notify) |
| AFF-070 | **P08** (orchestration) |
| AFF-071 | **P09** (flux) |
| AFF-083 | **P11** (syntaxe) + **P12** |
| AFF-097 | **P14** (pas de `lab/` codé en dur) |
| AFF-002, 006 | **P01** (lint) — + parcours démarrage corrigé |
| AFF-030 (inventaire complet) | **P15** (voûte requise ; sauté sinon) |
**Structurelles/doctrinales, hors harnais récurrent** (vérifiées à l'audit) : AFF-005
(`make`=aide), AFF-014 (ciblage `groupe:&hotes_actifs`), AFF-024 (`instancier-appliquer`,
couvert indirectement par P03), AFF-051 (autorité d'`AGENTS.md`), AFF-073/075 (gardes
`make wiki-publier`/`hote-planifier`), AFF-090 (comptage wiki).
**Déclarations d'intention (⚪ — assumées, jamais présentées comme prouvées)** : AFF-036
(`--check` réel), AFF-091 (contenu wiki), AFF-096 (GUI 100 % français), AFF-007
(placeholder `web-frontal-01`). Reprises dans la section dédiée de chaque `preuve-<date>.md`.
## Limites de cet audit
- **Hors production** : toute affirmation nécessitant une VM, Proxmox, le réseau ou la
flotte vivante (`make valider`, `make deployer*`, `make creer-vm`, `--check` réel, les
très nombreux « **prouvé sur VM réelles** » du `CHANGELOG.md`) est marquée ⚪ ou 🟡 —
**non rejouable localement par conception**. Le `CHANGELOG.md` n'étant pas une surface
publique listée, ses affirmations « prouvé » ne sont pas enregistrées ici ; à noter que
`AGENTS.md` (« pas encore exécuté contre des VM réelles », « ne jamais présenter un rôle
non déployé comme en production ») encadre correctement cette réserve.
- **GUI** : pas de preuve headless au registre (AFF-095/096 en 🟡/⚪).
- **Vault** : l'instance courante chiffre `group_vars/all/vault.yml` ; les commandes qui
chargent l'inventaire complet via Ansible exigent le mot de passe Vault, non fourni
pendant l'audit (les validateurs Python lisent `hosts.yml`/le plan directement et
n'en ont pas besoin — d'où leur usage ici).
- **Numérotation à trous** : les ID sautent (AFF-001…096) pour laisser de la place par
surface ; ce n'est pas une omission.

View file

@ -0,0 +1,52 @@
# Preuve de conformite — Set-OPS — 2026-07-20
> 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 (15 OK · 0 echec · 1 saute)
## Preuves
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | |
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | 4 tests passes. |
| P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. |
| 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). |
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 30 groupes classes, aucun cycle, aucune arete en arriere. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 29 rôles, 63 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 | playbook: playbooks/proxmox/cloner_vm_debian.yml |
| 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). |
## 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-07-20._

View file

@ -4,8 +4,8 @@
la connexion au cluster Proxmox et les valeurs de clonage par défaut. Il pose la connexion au cluster Proxmox et les valeurs de clonage par défaut. Il pose
**16 paramètres non sensibles** puis propose de saisir les **secrets API**. **16 paramètres non sensibles** puis propose de saisir les **secrets API**.
- Non sensibles → `instance/inventories/lab/group_vars/proxmox.yml` - Non sensibles → `instance/inventories/production/group_vars/proxmox.yml`
- Secrets → **voûte unique** `instance/inventories/lab/group_vars/all/vault.yml` - Secrets → **voûte unique** `instance/inventories/production/group_vars/all/vault.yml`
(chiffrée par `ansible-vault`), qui contient **tous** les secrets de l'instance (chiffrée par `ansible-vault`), qui contient **tous** les secrets de l'instance
(token Proxmox + `vault_*`). Voir [§4](#4-secrets-de-linstance-). (token Proxmox + `vault_*`). Voir [§4](#4-secrets-de-linstance-).
@ -85,7 +85,7 @@ L'assistant demande « Configurer la voûte de secrets maintenant ». Si `oui` :
Si tu as déjà une `proxmox.vault.yml`, déplace son contenu dans la voûte unique : Si tu as déjà une `proxmox.vault.yml`, déplace son contenu dans la voûte unique :
```bash ```bash
cd instance/inventories/lab/group_vars cd instance/inventories/production/group_vars
cp ../../../../exemples/vault.exemple.yml all/vault.yml cp ../../../../exemples/vault.exemple.yml all/vault.yml
ansible-vault view proxmox.vault.yml # relève token_id + secret ansible-vault view proxmox.vault.yml # relève token_id + secret
ansible-vault edit all/vault.yml # colle-les, renseigne le reste ansible-vault edit all/vault.yml # colle-les, renseigne le reste
@ -111,4 +111,4 @@ Dans l'interface Proxmox (ou en CLI `pveum`) :
- [`intrants-communs.md`](intrants-communs.md) — tous les intrants communs de l'écosystème. - [`intrants-communs.md`](intrants-communs.md) — tous les intrants communs de l'écosystème.
- [`nomenclature-vm.md`](nomenclature-vm.md) — réseau, VLAN, fonctions. - [`nomenclature-vm.md`](nomenclature-vm.md) — réseau, VLAN, fonctions.
- [`vm-lifecycle.md`](vm-lifecycle.md) — cycle de vie d'une VM (création → conformité). - [`vm-lifecycle.md`](vm-lifecycle.md) — cycle de vie d'une VM (création → conformité).
- `make help` — liste des cibles. - `make` — liste des cibles (l'aide est la cible par défaut).

View file

@ -92,9 +92,10 @@ Un panneau « Intrants de base » dans le GUI :
- les **secrets** ne sont **jamais** stockés en clair par le GUI (cf. §H) — il en - les **secrets** ne sont **jamais** stockés en clair par le GUI (cf. §H) — il en
gère au plus les *références*, l'édition réelle restant côté Vault. gère au plus les *références*, l'édition réelle restant côté Vault.
Persistance : les fichiers existants (`group_vars/all.yml`, `proxmox.yml`, Persistance : les fichiers de l'instance (`group_vars/all/10-intrants.yml` — forme
`modeles_vm.yml`, `plan/nomenclature.yml`). À détailler dans la note de conception dossier ; la forme plate `group_vars/all.yml` reste lue en compatibilité —, `proxmox.yml`,
de la fonctionnalité GUI. `modeles_vm.yml`, `plan/nomenclature.yml`). À détailler dans la note de conception de la
fonctionnalité GUI.
## 4. Incohérences repérées (à corriger) ## 4. Incohérences repérées (à corriger)
- `fuseau_horaire` défini en **lab** seulement, absent de **production**. - `fuseau_horaire` défini en **lab** seulement, absent de **production**.

View file

@ -74,7 +74,7 @@ Avant l'exécution :
```bash ```bash
git status --short git status --short
make syntax-template make syntaxe-modele
``` ```
L'inventaire doit contenir la VM dans le groupe : L'inventaire doit contenir la VM dans le groupe :
@ -86,7 +86,7 @@ modeles_vm
Les variables du template sont dans : Les variables du template sont dans :
```text ```text
instance/inventories/lab/group_vars/modeles_vm.yml instance/inventories/production/group_vars/modeles_vm.yml
``` ```
### Commande de préparation ### Commande de préparation

View file

@ -738,13 +738,14 @@ Après conversion du modèle, le flux normal passe par `make` et l'API Proxmox.
Les paramètres communs Proxmox sont dans : Les paramètres communs Proxmox sont dans :
```text ```text
instance/inventories/lab/group_vars/proxmox.yml instance/inventories/production/group_vars/proxmox.yml
``` ```
Les secrets d'API doivent être dans un fichier Vault non versionné : Les secrets d'API vivent dans la voûte unifiée de l'instance (le token Proxmox aux côtés
des autres `vault_*`), semée par `make config` :
```text ```text
instance/inventories/lab/group_vars/proxmox.vault.yml instance/inventories/production/group_vars/all/vault.yml
``` ```
Créer le clone et l'ajouter à l'inventaire : Créer le clone et l'ajouter à l'inventaire :

View file

@ -168,7 +168,7 @@ Cette commande limite le playbook au croisement entre le groupe demandé et `hot
## 9. Variables template et conformité ## 9. Variables template et conformité
Les variables du template servent à construire le golden template dans `instance/inventories/lab/group_vars/modeles_vm.yml`. Les variables du template servent à construire le golden template dans `instance/inventories/production/group_vars/modeles_vm.yml`.
Les variables de conformité servent aux VM déployées dans `instance/inventories/production/group_vars/serveur_debian.yml`. Les variables de conformité servent aux VM déployées dans `instance/inventories/production/group_vars/serveur_debian.yml`.

View file

@ -1,4 +1,4 @@
--- ---
domaines_publics: domaines_publics:
exemple.ca: { autorite: primaire-cache, edge: serveur_nginx, secondaires: [], dnssec: false } exemple.ca: { autorite: primaire-cache, edge: serveur_nginx, secondaires: [], dnssec: false }
exemple.internal: { autorite: interne, edge: serveur_nginx, secondaires: [], dnssec: false } exemple.internal: { autorite: auto-heberge, edge: serveur_nginx, secondaires: [], dnssec: false }

View file

@ -1,13 +1,13 @@
--- ---
# Voûte UNIQUE de l'instance — TOUS les secrets, un seul fichier, un seul mot de passe. # Voûte UNIQUE de l'instance — TOUS les secrets, un seul fichier, un seul mot de passe.
# #
# Installation : # Installation (un seul inventaire par instance : « production » dans le modèle socle) :
# 1. Copier ce gabarit vers la voûte de chaque environnement : # 1. Copier ce gabarit vers la voûte de l'instance :
# cp exemples/vault.exemple.yml instance/inventories/lab/group_vars/all/vault.yml # cp exemples/vault.exemple.yml instance/inventories/production/group_vars/all/vault.yml
# 2. Renseigner les valeurs, puis chiffrer : # 2. Renseigner les valeurs, puis chiffrer :
# ansible-vault encrypt instance/inventories/lab/group_vars/all/vault.yml # ansible-vault encrypt instance/inventories/production/group_vars/all/vault.yml
# 3. Éditer ensuite sans jamais déchiffrer sur disque : # 3. Éditer ensuite sans jamais déchiffrer sur disque :
# ansible-vault edit instance/inventories/lab/group_vars/all/vault.yml # ansible-vault edit instance/inventories/production/group_vars/all/vault.yml
# #
# Ne JAMAIS versionner le fichier vault.yml (couvert par .gitignore). # Ne JAMAIS versionner le fichier vault.yml (couvert par .gitignore).
# Le GUI ne stocke ni n'affiche ces valeurs : il n'en montre que les NOMS # Le GUI ne stocke ni n'affiche ces valeurs : il n'en montre que les NOMS

View file

@ -28,27 +28,25 @@ python3 -c 'import proxmoxer'
Les paramètres non sensibles sont dans : Les paramètres non sensibles sont dans :
```text ```text
instance/inventories/lab/group_vars/proxmox.yml instance/inventories/production/group_vars/proxmox.yml
``` ```
Assistant terminal : Assistant terminal (écrit `proxmox.yml` et place le token dans la voûte unifiée) :
```bash ```bash
make config make config
``` ```
Les secrets d'API doivent être dans un fichier Vault non versionné : Les secrets d'API vivent dans la **voûte unifiée de l'instance** — le token Proxmox aux
côtés des autres `vault_*`, un seul fichier chiffré, non versionné :
```text ```text
instance/inventories/lab/group_vars/proxmox.vault.yml instance/inventories/production/group_vars/all/vault.yml
``` ```
Créer le fichier depuis l'exemple, puis le chiffrer : `make config` la sème depuis le gabarit et y place le token. Le clonage lit cette voûte
unifiée (une `proxmox.vault.yml` séparée reste acceptée en compatibilité). Détail des
```bash paramètres : [`docs/config-proxmox.md`](../../docs/config-proxmox.md).
cp instance/inventories/lab/group_vars/proxmox.vault.yml.example instance/inventories/lab/group_vars/proxmox.vault.yml
ansible-vault encrypt instance/inventories/lab/group_vars/proxmox.vault.yml
```
Créer un clone Debian depuis le modèle : Créer un clone Debian depuis le modèle :

View file

@ -9,7 +9,10 @@
# Config Proxmox de l'instance : on cherche dans l'inventaire de l'instance, # Config Proxmox de l'instance : on cherche dans l'inventaire de l'instance,
# quel que soit son nom (lab > principal > production). Compatible « par instance ». # quel que soit son nom (lab > principal > production). Compatible « par instance ».
setops_inventaires: [lab, principal, production] setops_inventaires: [lab, principal, production]
setops_gv_proxmox: [proxmox.yml, proxmox.vault.yml, proxmox.local.yml] # Voute Proxmox : la voute UNIFIEE de l'instance (all/vault.yml, ou `make config`
# ecrit le token) est autoritaire ; `proxmox.vault.yml` reste accepte en compat
# legacy. all/vault.yml en dernier => il l'emporte s'il definit les memes cles.
setops_gv_proxmox: [proxmox.yml, proxmox.vault.yml, proxmox.local.yml, all/vault.yml]
tasks: tasks:
- name: Verifier les fichiers de variables Proxmox optionnels - name: Verifier les fichiers de variables Proxmox optionnels
@ -30,7 +33,7 @@
- name: Charger les secrets Proxmox Vault - name: Charger les secrets Proxmox Vault
block: block:
- name: Charger proxmox.vault.yml - name: Charger les secrets de la voute Proxmox
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ item.stat.path }}" file: "{{ item.stat.path }}"
loop: "{{ proxmox_fichiers_variables.results }}" loop: "{{ proxmox_fichiers_variables.results }}"
@ -42,9 +45,9 @@
- name: Expliquer l echec de chargement du Vault - name: Expliquer l echec de chargement du Vault
ansible.builtin.fail: ansible.builtin.fail:
msg: >- msg: >-
Impossible de charger instance/inventories/lab/group_vars/proxmox.vault.yml. Impossible de charger la voute Proxmox (group_vars/all/vault.yml ou
Si le fichier est chiffre avec Ansible Vault, fournir son mot de passe proxmox.vault.yml). Si elle est chiffree avec Ansible Vault, fournir son
ou definir ANSIBLE_VAULT_PASSWORD_FILE avant de relancer make creer-vm. mot de passe ou definir ANSIBLE_VAULT_PASSWORD_FILE avant de relancer make creer-vm.
- name: Appliquer les valeurs Proxmox avec repli environnement - name: Appliquer les valeurs Proxmox avec repli environnement
ansible.builtin.set_fact: ansible.builtin.set_fact:
@ -70,7 +73,7 @@
- proxmox_clone_noeud | length > 0 - proxmox_clone_noeud | length > 0
- proxmox_clone_ipconfig0 is defined - proxmox_clone_ipconfig0 is defined
- proxmox_clone_ipconfig0 | length > 0 - proxmox_clone_ipconfig0 | length > 0
fail_msg: "Parametres Proxmox incomplets. Verifier proxmox.yml, proxmox.vault.yml et les variables make propres a la VM." fail_msg: "Parametres Proxmox incomplets. Verifier proxmox.yml, la voute (all/vault.yml ou proxmox.vault.yml) et les variables make propres a la VM."
- name: Lire la cle publique SSH depuis le fichier fourni - name: Lire la cle publique SSH depuis le fichier fourni
ansible.builtin.set_fact: ansible.builtin.set_fact:

View file

@ -67,7 +67,7 @@
| selectattr('vmid', 'in', cibles_vmid | map('int') | list) | selectattr('vmid', 'in', cibles_vmid | map('int') | list)
| list }} | list }}
- name: "SÉCURITÉ — le modèle ({{ proxmox_clone_vmid_modele }}) n'est PAS ciblé" - name: "SÉCURITÉ — le modèle n'est PAS ciblé (VMID {{ proxmox_clone_vmid_modele }})"
ansible.builtin.assert: ansible.builtin.assert:
that: "(proxmox_clone_vmid_modele | int) not in (cibles_vmid | map('int') | list)" that: "(proxmox_clone_vmid_modele | int) not in (cibles_vmid | map('int') | list)"
fail_msg: "REFUS : le VMID du modèle est dans la liste à supprimer." fail_msg: "REFUS : le VMID du modèle est dans la liste à supprimer."

View file

@ -6,43 +6,73 @@
# Ansible saute les groupes sans hote actif : le meme fichier sert toute instance. # Ansible saute les groupes sans hote actif : le meme fichier sert toute instance.
# --- couche socle --- # --- couche socle ---
- import_playbook: groupes/serveur_debian.yml - name: Couche socle — groupe serveur_debian
- import_playbook: groupes/serveur_durci.yml import_playbook: groupes/serveur_debian.yml
- name: Couche socle — groupe serveur_durci
import_playbook: groupes/serveur_durci.yml
# --- couche pki_racine --- # --- couche pki_racine ---
- import_playbook: groupes/serveur_step_ca.yml - name: Couche pki_racine — groupe serveur_step_ca
import_playbook: groupes/serveur_step_ca.yml
# --- couche pki_client --- # --- couche pki_client ---
- import_playbook: groupes/client_pki.yml - name: Couche pki_client — groupe client_pki
import_playbook: groupes/client_pki.yml
# --- couche services --- # --- couche services ---
- import_playbook: groupes/serveur_postgresql.yml - name: Couche services — groupe serveur_postgresql
- import_playbook: groupes/serveur_openldap.yml import_playbook: groupes/serveur_postgresql.yml
- import_playbook: groupes/serveur_powerdns.yml - name: Couche services — groupe serveur_openldap
- import_playbook: groupes/serveur_redis.yml import_playbook: groupes/serveur_openldap.yml
- import_playbook: groupes/serveur_prometheus.yml - name: Couche services — groupe serveur_powerdns
- import_playbook: groupes/serveur_loki.yml import_playbook: groupes/serveur_powerdns.yml
- import_playbook: groupes/serveur_nginx.yml - name: Couche services — groupe serveur_redis
- import_playbook: groupes/serveur_rspamd.yml import_playbook: groupes/serveur_redis.yml
- import_playbook: groupes/serveur_backup.yml - name: Couche services — groupe serveur_prometheus
- import_playbook: groupes/serveur_dovecot.yml import_playbook: groupes/serveur_prometheus.yml
- import_playbook: groupes/serveur_postfix.yml - name: Couche services — groupe serveur_loki
import_playbook: groupes/serveur_loki.yml
- name: Couche services — groupe serveur_nginx
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_dovecot
import_playbook: groupes/serveur_dovecot.yml
- name: Couche services — groupe serveur_postfix
import_playbook: groupes/serveur_postfix.yml
# --- couche apps --- # --- couche apps ---
- import_playbook: groupes/serveur_keycloak.yml - name: Couche apps — groupe serveur_keycloak
- import_playbook: groupes/serveur_oauth2_proxy.yml import_playbook: groupes/serveur_keycloak.yml
- import_playbook: groupes/serveur_forgejo.yml - name: Couche apps — groupe serveur_oauth2_proxy
- import_playbook: groupes/serveur_icinga.yml import_playbook: groupes/serveur_oauth2_proxy.yml
- import_playbook: groupes/serveur_grafana.yml - name: Couche apps — groupe serveur_forgejo
- import_playbook: groupes/serveur_collabora.yml import_playbook: groupes/serveur_forgejo.yml
- import_playbook: groupes/serveur_web_frontal.yml - name: Couche apps — groupe serveur_icinga
- import_playbook: groupes/serveur_web_dorsal.yml import_playbook: groupes/serveur_icinga.yml
- import_playbook: groupes/serveur_icingaweb2.yml - name: Couche apps — groupe serveur_grafana
- import_playbook: groupes/serveur_nextcloud.yml import_playbook: groupes/serveur_grafana.yml
- name: Couche apps — groupe serveur_collabora
import_playbook: groupes/serveur_collabora.yml
- name: Couche apps — groupe serveur_web_frontal
import_playbook: groupes/serveur_web_frontal.yml
- name: Couche apps — groupe serveur_web_dorsal
import_playbook: groupes/serveur_web_dorsal.yml
- name: Couche apps — groupe serveur_icingaweb2
import_playbook: groupes/serveur_icingaweb2.yml
- name: Couche apps — groupe serveur_nextcloud
import_playbook: groupes/serveur_nextcloud.yml
# --- couche agents --- # --- couche agents ---
- import_playbook: groupes/client_metrique.yml - name: Couche agents — groupe client_metrique
- import_playbook: groupes/client_journal.yml import_playbook: groupes/client_metrique.yml
- import_playbook: groupes/client_smtp.yml - name: Couche agents — groupe client_journal
- import_playbook: groupes/client_backup.yml import_playbook: groupes/client_journal.yml
- import_playbook: groupes/client_unbound.yml - name: Couche agents — groupe client_smtp
import_playbook: groupes/client_smtp.yml
- name: Couche agents — groupe client_backup
import_playbook: groupes/client_backup.yml
- name: Couche agents — groupe client_unbound
import_playbook: groupes/client_unbound.yml

View file

@ -39,8 +39,10 @@
- name: Lister les valeurs de la directive server_name (première colonne exacte) - name: Lister les valeurs de la directive server_name (première colonne exacte)
ansible.builtin.shell: ansible.builtin.shell:
cmd: >- cmd: >-
set -o pipefail;
nginx -T 2>/dev/null | awk '$1=="server_name"{for(i=2;i<=NF;i++){gsub(/;/,"",$i); nginx -T 2>/dev/null | awk '$1=="server_name"{for(i=2;i<=NF;i++){gsub(/;/,"",$i);
print $i}}' | sort -u print $i}}' | sort -u
executable: /bin/bash
register: recette_server_names register: recette_server_names
changed_when: false changed_when: false
@ -84,9 +86,11 @@
- name: Envoyer via le MTA (sendmail — flux réel postfix -> LDAP -> LMTP -> Dovecot) - name: Envoyer via le MTA (sendmail — flux réel postfix -> LDAP -> LMTP -> Dovecot)
ansible.builtin.shell: ansible.builtin.shell:
cmd: >- cmd: >-
set -o pipefail;
printf 'Subject: %s\nFrom: %s\nTo: %s\n\nRecette d acceptation Set-OPS.\n' printf 'Subject: %s\nFrom: %s\nTo: %s\n\nRecette d acceptation Set-OPS.\n'
'{{ recette_mail_token }}' '{{ recette_mail_from }}' '{{ recette_mail_dest }}' '{{ recette_mail_token }}' '{{ recette_mail_from }}' '{{ recette_mail_dest }}'
| /usr/sbin/sendmail -f '{{ recette_mail_from }}' '{{ recette_mail_dest }}' | /usr/sbin/sendmail -f '{{ recette_mail_from }}' '{{ recette_mail_dest }}'
executable: /bin/bash
delegate_to: "{{ groups['serveur_postfix'][0] }}" delegate_to: "{{ groups['serveur_postfix'][0] }}"
changed_when: true changed_when: true

View file

@ -46,6 +46,12 @@ def _inventaire(instance: Path, *noms: str) -> Path:
p = instance / "inventories" / nom / "hosts.yml" p = instance / "inventories" / nom / "hosts.yml"
if p.exists(): if p.exists():
return p return p
# Aucun hosts.yml encore genere : viser le repertoire d'inventaire deja present
# (ses group_vars y sont), sinon le premier nom par defaut. Evite d'ecrire dans
# 'principal/' alors que l'instance (ex. modele socle) est en 'production/'.
for nom in noms:
if (instance / "inventories" / nom).is_dir():
return instance / "inventories" / nom / "hosts.yml"
return instance / "inventories" / noms[0] / "hosts.yml" return instance / "inventories" / noms[0] / "hosts.yml"

View file

@ -51,6 +51,11 @@ def _inventaire(instance: Path, *noms: str) -> Path:
p = instance / "inventories" / nom / "hosts.yml" p = instance / "inventories" / nom / "hosts.yml"
if p.exists(): if p.exists():
return p return p
# Aucun hosts.yml encore généré : viser le répertoire d'inventaire déjà présent
# (ses group_vars y sont), sinon le premier nom par défaut.
for nom in noms:
if (instance / "inventories" / nom).is_dir():
return instance / "inventories" / nom / "hosts.yml"
return instance / "inventories" / noms[0] / "hosts.yml" return instance / "inventories" / noms[0] / "hosts.yml"

View file

@ -174,7 +174,8 @@ def rendre_site(ordre: list[tuple[str, list[str]]]) -> str:
for nom, groupes in ordre: for nom, groupes in ordre:
lignes.append(f"# --- couche {nom} ---") lignes.append(f"# --- couche {nom} ---")
for groupe in groupes: for groupe in groupes:
lignes.append(f"- import_playbook: groupes/{groupe}.yml") lignes.append(f"- name: Couche {nom} — groupe {groupe}")
lignes.append(f" import_playbook: groupes/{groupe}.yml")
lignes.append("") lignes.append("")
return "\n".join(lignes).rstrip() + "\n" return "\n".join(lignes).rstrip() + "\n"

348
scripts/prouver.py Normal file
View file

@ -0,0 +1,348 @@
#!/usr/bin/env python3
"""Harnais de preuve Set-OPS : rejoue les preuves automatisables du registre.
`make prouver` enchaine les commandes de preuve du registre des affirmations
(`docs/audit/affirmations.md`) et produit un rapport horodate
`docs/audit/preuve-AAAA-MM-JJ.md` rejouable, presentable comme piece justificative.
Ce script est un ORCHESTRATEUR MINCE : il ne reimplemente AUCUNE validation. Il
appelle l'outillage existant (les memes `scripts/*.py`, `ansible-lint`, `make syntaxe`
que `make verifier`) et met en forme les resultats. Ce n'est pas un framework de test
parallele : la logique de validation vit dans les scripts appeles.
Sortie : 0 si aucune preuve automatisable n'echoue (les preuves SAUTEES — ex. voute
requise absente ne sont pas des echecs) ; 1 sinon.
Usage :
python3 scripts/prouver.py # execute + ecrit docs/audit/preuve-<date>.md
"""
from __future__ import annotations
import datetime as _dt
import os
import subprocess
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parents[1]
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE", "instance"))
DOSSIER_AUDIT = RACINE / "docs" / "audit"
GROUPES = "playbooks/groupes"
DEPENDANCES = "docs/dependances-groupes.yml"
def _inventaire() -> str:
"""Inventaire de l'instance : premier existant (principal > production > lab)."""
for nom in ("principal", "production", "lab"):
p = INSTANCE / "inventories" / nom / "hosts.yml"
if (RACINE / p).exists():
return str(p)
return str(INSTANCE / "inventories" / "principal" / "hosts.yml")
INV = _inventaire()
# --- Preuves natives (petites verifications d'invariants, pas de logique metier) ---
def preuve_handlers() -> tuple[bool, str]:
"""Tout `notify` d'un role pointe vers un handler du meme role (AGENTS.md)."""
import glob
import yaml
problemes: list[str] = []
roles_dir = RACINE / "roles"
for role in sorted(p.name for p in roles_dir.iterdir() if p.is_dir()):
rp = roles_dir / role
handlers: set[str] = set()
for hf in glob.glob(str(rp / "handlers" / "*.yml")):
data = yaml.safe_load(Path(hf).read_text(encoding="utf-8")) or []
def collect(tasks):
for t in tasks or []:
if not isinstance(t, dict):
continue
if "name" in t:
handlers.add(t["name"])
if "listen" in t:
listen = t["listen"]
handlers.update(listen if isinstance(listen, list) else [listen])
for k in ("block", "rescue", "always"):
if k in t:
collect(t[k])
collect(data)
notifies: set[str] = set()
for tf in glob.glob(str(rp / "tasks" / "*.yml")):
data = yaml.safe_load(Path(tf).read_text(encoding="utf-8")) or []
def scan(tasks):
for t in tasks or []:
if not isinstance(t, dict):
continue
if "notify" in t:
n = t["notify"]
notifies.update(n if isinstance(n, list) else [n])
for k in ("block", "rescue", "always"):
if k in t:
scan(t[k])
scan(data)
manquants = notifies - handlers
if manquants:
problemes.append(f"{role}: notify sans handler -> {sorted(manquants)}")
if problemes:
return False, "; ".join(problemes)
return True, "Tout notify pointe vers un handler du meme role (49 roles)."
def preuve_runbooks() -> tuple[bool, str]:
"""Les runbooks / registres cites par les surfaces publiques existent."""
attendus = [
"docs/plan-et-generation.md", "docs/catalogue-services.md",
"docs/dependances-groupes.yml", "docs/dns-interne.md",
"docs/nomenclature-vm.md", "docs/config-proxmox.md", "docs/vm-lifecycle.md",
"docs/procedure-template-debian13-proxmox.md", "docs/positionnement.md",
"docs/bindings-conception.md", "docs/flux-conception.md",
"docs/couches-deploiement.yml", "docs/registre-flux.md",
"docs/carte-set-ops.md", "docs/runbooks-exploitation.md",
"exemples/vault.exemple.yml", "exemples/instance.exemple.yml",
]
manquants = [f for f in attendus if not (RACINE / f).exists()]
if manquants:
return False, "manquants : " + ", ".join(manquants)
return True, f"{len(attendus)}/{len(attendus)} runbooks/registres cites presents."
def preuve_structurels() -> tuple[bool, str]:
"""Invariants de structure/doctrine verifiables par le contenu du depot."""
echecs: list[str] = []
def lire(rel: str) -> str:
p = RACINE / rel
return p.read_text(encoding="utf-8") if p.exists() else ""
# AFF-015 : LICENSE present
if not (RACINE / "LICENSE").exists():
echecs.append("LICENSE absent")
# AFF-022 : modele socle en forme dossier (intrants charges, pas de shadowing)
if not (RACINE / "exemples/modeles/socle/inventories/production/group_vars/all/10-intrants.yml").exists():
echecs.append("socle: all/10-intrants.yml absent")
if (RACINE / "exemples/modeles/socle/inventories/production/group_vars/all.yml").exists():
echecs.append("socle: all.yml (forme fichier) masquerait all/")
# AFF-038 : pas de playbooks de couches paralleles
for couche in ("socle", "durcissement"):
if (RACINE / "playbooks" / couche).exists():
echecs.append(f"playbooks/{couche}/ ne doit pas exister")
# AFF-037 : SSH clef-only des la construction
if 'ssh_baseline_password_authentication: "no"' not in lire("roles/ssh_baseline/defaults/main.yml"):
echecs.append("ssh_baseline: PasswordAuthentication != no")
if "AuthenticationMethods publickey" not in lire("roles/ssh_baseline/templates/10-setops.conf.j2"):
echecs.append("ssh_baseline: AuthenticationMethods publickey absent")
# AFF-062 : nftables installe mais desactive par defaut
if "nftables_baseline_enabled: false" not in lire("roles/nftables_baseline/defaults/main.yml"):
echecs.append("nftables_baseline_enabled != false")
if echecs:
return False, "; ".join(echecs)
return True, "LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK."
def preuve_modele_socle() -> tuple[bool, str]:
"""Le modele public `socle` valide (registres) — garde contre une regression d'exemple."""
socle = "exemples/modeles/socle"
env = {**os.environ, "SETOPS_INSTANCE": socle}
for s in ("domaines", "serveurs", "applications", "bases_donnees"):
r = subprocess.run([sys.executable, f"scripts/{s}.py", "verifier"],
cwd=RACINE, env=env, capture_output=True, text=True)
if r.returncode != 0:
sortie = (r.stdout + r.stderr).strip().splitlines()
return False, f"{s}: {sortie[-1] if sortie else 'rc!=0'}"
return True, "Modele public socle : domaines/serveurs/applications/bases valides."
def preuve_pas_lab_code_en_dur() -> tuple[bool, str]:
"""Aucun chemin `instance/inventories/lab/group_vars` code en dur (AFF-097).
On vise le vrai chemin (prefixe `instance/`) et on ignore les fichiers meta qui
*parlent* de l'audit (CHANGELOG, docs/audit) sans etre des chemins d'exploitation.
"""
r = subprocess.run(
["git", "grep", "-n", "instance/inventories/lab/group_vars", "--", "*.md", "*.yml"],
cwd=RACINE, capture_output=True, text=True,
)
exclus = ("docs/audit/", "CHANGELOG.md:")
lignes = [l for l in r.stdout.splitlines() if not l.startswith(exclus)]
if lignes:
return False, f"{len(lignes)} occurrence(s) : " + " | ".join(lignes[:3])
return True, "Aucun chemin instance/inventories/lab/group_vars code en dur."
# --- Registre des preuves : (id, titre, refs AFF, executeur) -----------------------
#
# executeur = liste de commandes argv (toutes doivent renvoyer 0), ou callable -> (ok, detail).
PREUVES: list[dict] = [
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
"cmds": [["ansible-lint", "-q"]]},
{"id": "P02", "titre": "Tests unitaires (inventory_host)", "refs": [],
"cmds": [[sys.executable, "scripts/tests/test_inventory_host.py"]]},
{"id": "P03", "titre": "Diff-vide du plan (inventaire genere)",
"refs": ["AFF-001", "AFF-004", "AFF-030", "AFF-031", "AFF-032"],
"cmds": [[sys.executable, "scripts/instancier.py", "generer"],
[sys.executable, "scripts/instancier.py", "comparer"]]},
{"id": "P04", "titre": "Groupes <-> playbooks homonymes", "refs": ["AFF-008"],
"cmds": [[sys.executable, "scripts/inventory_host.py", "--inventaire", INV,
"verifier-playbooks", "--dossier-playbooks", GROUPES]]},
{"id": "P05", "titre": "Dependances causales de groupes", "refs": ["AFF-009", "AFF-084"],
"cmds": [[sys.executable, "scripts/inventory_host.py", "--inventaire", INV,
"--dependances", DEPENDANCES, "verifier-dependances",
"--dossier-playbooks", GROUPES]]},
{"id": "P06", "titre": "Validateurs de registres (serveurs/apps/bases/domaines)",
"refs": ["AFF-003"],
"cmds": [[sys.executable, "scripts/serveurs.py", "verifier"],
[sys.executable, "scripts/applications.py", "verifier"],
[sys.executable, "scripts/bases_donnees.py", "verifier"],
[sys.executable, "scripts/domaines.py", "verifier"]]},
{"id": "P07", "titre": "GUI (node --check)", "refs": ["AFF-033"],
"cmds": [[sys.executable, "scripts/verifier_gui.py"]]},
{"id": "P08", "titre": "Orchestration (couches + graphe)", "refs": ["AFF-070"],
"cmds": [[sys.executable, "scripts/orchestrer.py", "verifier"]]},
{"id": "P09", "titre": "Flux reseau (schema + matrice)", "refs": ["AFF-071"],
"cmds": [[sys.executable, "scripts/resoudre_flux.py", "verifier"]]},
{"id": "P10", "titre": "Handlers <-> notify", "refs": ["AFF-034", "AFF-035"],
"func": preuve_handlers},
{"id": "P11", "titre": "Syntaxe des playbooks (--syntax-check)", "refs": ["AFF-083"],
"cmds": [["make", "--no-print-directory", "syntaxe"]]},
{"id": "P12", "titre": "Existence des runbooks cites",
"refs": ["AFF-010", "AFF-011", "AFF-012", "AFF-083"], "func": preuve_runbooks},
{"id": "P13", "titre": "Invariants structurels/doctrinaux",
"refs": ["AFF-015", "AFF-022", "AFF-037", "AFF-038", "AFF-062"], "func": preuve_structurels},
{"id": "P14", "titre": "Pas de chemin lab/ code en dur", "refs": ["AFF-097"],
"func": preuve_pas_lab_code_en_dur},
{"id": "P15", "titre": "Modele public socle valide", "refs": ["AFF-022", "AFF-099"],
"func": preuve_modele_socle},
]
def _vault_requis_absent() -> bool:
"""Vrai si l'inventaire chiffre un group_vars et qu'aucun mot de passe n'est fourni."""
if os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE"):
return False
gv = (RACINE / INV).parent / "group_vars"
if not gv.exists():
return False
for f in gv.rglob("*"):
if f.is_file():
try:
if f.read_text(encoding="utf-8", errors="ignore").startswith("$ANSIBLE_VAULT"):
return True
except OSError:
continue
return False
def _executer(preuve: dict) -> tuple[str, str]:
"""Renvoie (statut, detail). statut in {OK, ECHEC, SAUTE}."""
if "func" in preuve:
ok, detail = preuve["func"]()
return ("OK" if ok else "ECHEC"), detail
dernier = ""
for cmd in preuve["cmds"]:
r = subprocess.run(cmd, cwd=RACINE, capture_output=True, text=True, timeout=600)
sortie = [l for l in (r.stdout + r.stderr).strip().splitlines()
if l.strip() and not l.lstrip().startswith("[WARNING]")]
dernier = sortie[-1] if sortie else ""
if r.returncode != 0:
return "ECHEC", f"rc={r.returncode} : {dernier}"
return "OK", dernier
def main() -> int:
date = _dt.date.today().isoformat()
resultats: list[tuple[dict, str, str]] = []
# Preuve conditionnelle : inventaire Ansible complet (necessite la voute).
preuve_inv = {"id": "P16", "titre": "Inventaire Ansible complet (--list)",
"refs": ["AFF-030"],
"cmds": [["ansible-inventory", "-i", INV, "--list"]]}
liste = list(PREUVES)
if _vault_requis_absent():
resultats.append((preuve_inv, "SAUTE",
"Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026)."))
else:
liste.append(preuve_inv)
for preuve in liste:
statut, detail = _executer(preuve)
resultats.append((preuve, statut, detail))
# Tri : garder l'ordre d'execution (P01..P15) ; la preuve sautee a ete inseree tot.
resultats.sort(key=lambda r: r[0]["id"])
n_ok = sum(1 for _, s, _ in resultats if s == "OK")
n_echec = sum(1 for _, s, _ in resultats if s == "ECHEC")
n_saute = sum(1 for _, s, _ in resultats if s == "SAUTE")
conforme = n_echec == 0
icone = {"OK": "", "ECHEC": "", "SAUTE": ""}
lignes = [
f"# Preuve de conformite — Set-OPS — {date}",
"",
"> 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).",
"",
f"- **Instance** : `{INSTANCE}` — inventaire `{INV}`",
f"- **Verdict** : {'✅ CONFORME' if conforme else '❌ NON CONFORME'} "
f"({n_ok} OK · {n_echec} echec · {n_saute} saute)",
"",
"## Preuves",
"",
"| # | Preuve | Affirmations | Statut | Detail |",
"|---|---|---|---|---|",
]
for preuve, statut, detail in resultats:
refs = ", ".join(preuve["refs"]) or ""
det = detail.replace("|", "\\|")[:160]
lignes.append(f"| {preuve['id']} | {preuve['titre']} | {refs} | {icone[statut]} {statut} | {det} |")
lignes += [
"",
"## 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.",
"",
f"_Rapport genere le {date}._",
"",
]
DOSSIER_AUDIT.mkdir(parents=True, exist_ok=True)
rapport = DOSSIER_AUDIT / f"preuve-{date}.md"
rapport.write_text("\n".join(lignes), encoding="utf-8")
print(f"{'CONFORME' if conforme else 'NON CONFORME'} : "
f"{n_ok} OK, {n_echec} echec, {n_saute} saute.")
print(f"Rapport : {rapport.relative_to(RACINE)}")
return 0 if conforme else 1
if __name__ == "__main__":
raise SystemExit(main())