Le devis frontière annonçait trois routes de retour « déjà émises par devis-reseau ». Le devis switch n'en émettait qu'une : il lisait `nftables_admin_ssh` de la seule instance active, alors que la frontière était passée multi-tenant. Les réseaux d'administration de Technolibre n'étaient routés nulle part — et une affirmation fausse est pire qu'un silence, elle désamorce la vérification. `admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au lieu d'en refaire une copie : routes de retour et règles lisent les mêmes tenants par construction. Vérifié identiques. Ajouté : l'avertissement « Block private networks ». Le SSH d'administration a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif par défaut et s'applique AVANT les règles — coché, il jette le paquet sans qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans les règles, il fallait l'écrire à part. Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait été trop large (documentation, CGNAT) et l'avertissement se serait déclenché à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet. Preuves : 24 OK, 0 échec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2050 lines
148 KiB
Markdown
2050 lines
148 KiB
Markdown
# CHANGELOG — Set-OPS
|
||
|
||
## 2026-08-02 (suite 4) — deux devis qui se contredisaient, et une case à cocher
|
||
|
||
### Corrigé — le devis frontière certifiait des routes inexistantes
|
||
Sa section 0 annonçait trois routes de retour « DÉJÀ ÉMISES par `make devis-reseau` ». Le
|
||
devis switch n'en émettait **qu'une** : `devis_reseau` lisait `nftables_admin_ssh` de la
|
||
seule instance active, alors que la frontière était passée multi-tenant la veille. Les deux
|
||
réseaux d'administration de Technolibre n'étaient routés nulle part.
|
||
|
||
Pire qu'un silence : une affirmation fausse désamorce la vérification.
|
||
|
||
`admin_tous_tenants()` vit désormais dans `devis_reseau` et **`devis_opnsense` l'importe**
|
||
au lieu d'en refaire une copie. Les routes de retour et les règles lisent les mêmes tenants,
|
||
par construction. Vérifié : les deux listes sont identiques.
|
||
|
||
### Ajouté — l'avertissement « Block private networks »
|
||
Le SSH d'administration a une source RFC1918 arrivant sur une interface **WAN**. OPNsense
|
||
active par défaut ce filtre d'interface, qui s'applique **avant** les règles : coché, il jette
|
||
le paquet sans qu'aucune règle ne soit consultée. La config paraît juste, le SSH ne passe pas.
|
||
|
||
Le devis le signale dès qu'une source RFC1918 entre par le WAN — un réglage d'interface est
|
||
invisible dans les règles, il fallait donc l'écrire à part.
|
||
|
||
Le prédicat est **exactement** RFC1918, périmètre de cette case ; `ipaddress.is_private`
|
||
aurait été trop large (plages de documentation, CGNAT), et l'avertissement se serait déclenché
|
||
à tort. Les trois cas exercés : RFC1918 → averti ; `8.8.8.8/32` → muet ; `203.0.113.7/32`
|
||
(documentation) → muet.
|
||
|
||
## 2026-08-02 (suite 3) — chaque règle porte son interface, et l'octet est gardé
|
||
|
||
### Ajouté — l'interface d'arrivée, dérivée du sens du flux
|
||
Dans OPNsense une règle est **toujours `in` sur l'interface d'arrivée** : posée ailleurs, elle
|
||
ne s'applique jamais et le trafic est bloqué sans que la configuration paraisse anormale. Les
|
||
règles n'en portaient aucune, alors que le champ est obligatoire dans l'API.
|
||
|
||
L'attribution se dérive : un flux `ingress`/`externe` arrive par le **WAN**, un flux
|
||
`egress`/`externe` par le **lien de transit**. Le SSH d'administration suit la première ligne
|
||
— le VPN est hébergé sur le pfSense voisin et revient par l'adresse publique de la frontière.
|
||
C'était la dernière inconnue, et le montage parallèle décrit le 2026-08-02 la lève.
|
||
|
||
Le rendu abandonne `pass out` pour `pass in on <interface>`, qui est l'idiome réel d'OPNsense
|
||
et ce que le futur client d'API devra envoyer.
|
||
|
||
### Ajouté — `opnsense_wan_ip`, la face publique
|
||
L'adresse publique de la frontière (`69.70.26.62`, reprise du pfSense) est un intrant de la
|
||
section *Frontière* et s'affiche en section 1 du devis.
|
||
|
||
### Ajouté — invariant du dernier octet (preuve P23)
|
||
Convention d'exploitation : un point de routage porte **le même dernier octet sur tous les
|
||
sous-réseaux où il participe** — on retient une adresse, pas treize. `sleipnir-01` est `.1`
|
||
partout. Le chiffre n'est pas codé en dur : il vient de `reservations.passerelle` dans la
|
||
nomenclature, et `make underlay` refuse une passerelle qui s'en écarte.
|
||
|
||
Exemption assumée : les liens plus étroits qu'un `/24`. Sur le `/29` de transit, l'adressage
|
||
est dicté par les participants — les deux frontières prennent `.1` et `.2`, le switch `.6`.
|
||
Vérifié que l'invariant était **déjà respecté** sur les 13 sous-réseaux routés avant d'écrire
|
||
la garde.
|
||
|
||
## 2026-08-02 (suite 2) — la frontière porte les règles de TOUS les tenants
|
||
|
||
Le devis était multi-tenant pour ses routes et mono-tenant pour ses règles : il routait
|
||
`10.21.0.0/16` et `10.27.0.0/16`, mais ne filtrait que l'instance active. Technolibre aurait
|
||
été routé jusqu'à la bordure puis bloqué dans les deux sens, SSH d'administration compris,
|
||
sans qu'aucune ligne ne dise pourquoi. Chemin présent, politique absente — le mode de panne
|
||
du 2026-07-29, transposé.
|
||
|
||
La résolution est désormais paramétrée par tenant : `inventaire_de()` lit le `hosts.yml` de
|
||
chaque instance fédérée, `cibles_par_role()` prend l'inventaire en argument, et les alias
|
||
d'hôtes sont préfixés (`SETOPS_CHEZ17_SERVEUR_NGINX`). 11 règles par tenant, 22 au total.
|
||
|
||
### Cloisonnement du plan de gestion
|
||
Première version de ce correctif : `SETOPS_ADMIN` devenait l'**union** des réseaux
|
||
d'administration. Le plan de gestion de Technolibre aurait alors pu entrer en SSH chez
|
||
Chezlepro — la bordure rouvrait ce que les ACL de switch ferment. Corrigé avant livraison :
|
||
**un alias par tenant**, `SETOPS_ADMIN_<TENANT>`, n'ouvrant que son propre supernet.
|
||
|
||
L'union est conservée pour les routes de **retour** côté switch et la garde P24 : router
|
||
n'est pas autoriser, et le switch doit savoir revenir vers tous les plans de gestion.
|
||
|
||
### Deux omissions annoncées au lieu d'être tues
|
||
Un tenant sans inventaire généré : aucune règle, et le devis le dit. Un tenant dont
|
||
`nftables_admin_ssh` est vide : la règle SSH est **omise** plutôt qu'ouverte à `any`, ce qui
|
||
exposerait le SSH à Internet. Cas dégradé exercé.
|
||
|
||
## 2026-08-02 (suite) — la sortie générale est déclarée, pas subie
|
||
|
||
Le devis frontière se terminait par `block out log all` avec **une seule** règle sortante
|
||
(le relais SMTP). Appliqué tel quel, il coupait la flotte d'Internet : plus de `apt`, plus
|
||
de NTP, plus de récursion DNS. Rien ne le signalait — c'était la ligne la plus lourde de
|
||
conséquences du devis, posée à la suite des autres.
|
||
|
||
La sortie est désormais **déclarée dans le registre**, donc dérivée comme tout le reste :
|
||
|
||
- `serveur_debian` (le socle, porté par les 14 hôtes) — `443/tcp` et `80/tcp` pour les
|
||
dépôts apt, `123/udp` pour l'horloge. Une dérive d'horloge fait échouer la validation des
|
||
certificats step-ca et le SSO, des semaines après la cause.
|
||
- `client_unbound` — `53/udp` et `53/tcp` : `client_unbound_transitaires` est vide, donc
|
||
Unbound interroge lui-même la racine. C'est le choix souverain ; il a un coût réseau qu'il
|
||
faut déclarer. Le TCP n'est pas optionnel — c'est le repli obligatoire dès qu'une réponse
|
||
DNSSEC dépasse la taille UDP.
|
||
|
||
Le devis passe de 6 à 11 règles, et sa section 5 **énonce** le default-deny sortant, le
|
||
nombre de règles qui l'accompagnent, et où déclarer un besoin oublié — jamais à la main dans
|
||
le pare-feu, la règle serait perdue à la génération suivante.
|
||
|
||
Vérifié : les nftables d'hôte sont **inchangés**, octet pour octet. Le pair `externe` reste
|
||
sauté par `resoudre_flux.py` — ces flux relèvent de la bordure, et d'elle seule.
|
||
|
||
Non déclaré volontairement : le rôle `chrony` existe mais n'est référencé par aucun groupe,
|
||
aucun playbook ni le graphe de dépendances. Lui écrire un flux aurait créé une règle morte.
|
||
|
||
## 2026-08-02 — les interfaces se nomment par leur identifiant
|
||
|
||
`opt1`, `igb1` et `TENANTS` désignent le même port dans OPNsense : l'identifiant interne, le
|
||
périphérique FreeBSD et le libellé affiché. L'API REST ne parle que du **premier**, et c'est
|
||
lui que veulent `opnsense_if_wan` et `opnsense_if_transit`. Le libellé de ces deux intrants ne
|
||
le disait pas — la question s'est posée en pratique.
|
||
|
||
Les libellés le disent maintenant explicitement, et `docs/frontiere-opnsense.md` §6 explique
|
||
les trois couches ainsi que le motif du choix : `opt1` est le plus stable des trois, il
|
||
survit à un changement de carte réseau comme à un renommage.
|
||
|
||
La note « `opnsense_prochain_saut` dérive de l'underlay » est repliée dans l'en-tête que le
|
||
panneau régénère : une sauvegarde l'effaçait, puisque le fichier est réécrit depuis le YAML
|
||
analysé. Vérifié qu'une sauvegarde préserve valeurs **et** références de voûte.
|
||
|
||
### Corrigé — la doc portait encore l'ancien plan du `/29`
|
||
Après le renumérotage (`bifrost-1/-2` en `.1`/`.2`, SVI en `.6`), deux passages de
|
||
`docs/frontiere-opnsense.md` annonçaient toujours `10.0.4.2` comme prochain saut. La doc
|
||
contredisait le devis généré ; les `ip route` des deux coïncident désormais.
|
||
|
||
## 2026-08-01 (suite 14) — l'empreinte du root CA n'est pas un secret de voûte
|
||
|
||
`client_pki` **dérive l'empreinte à chaud** depuis l'autorité (`step certificate
|
||
fingerprint` en `delegate_to` sur `serveur_step_ca`) — précisément parce qu'un `from-zero`
|
||
régénère l'AC avec une empreinte neuve. Une empreinte figée en voûte serait périmée dès la
|
||
première reconstruction, et une empreinte périmée fait échouer le `bootstrap` de chaque hôte.
|
||
|
||
Or `defaults/main.yml` portait encore `client_pki_ca_fingerprint: "{{
|
||
vault_step_ca_fingerprint | default('') }}"`. Ce défaut était **mort** : la tâche suivante
|
||
écrase le fait sans condition. La valeur de la voûte n'avait aucun effet, quel qu'en soit le
|
||
contenu.
|
||
|
||
Le recensement de `voute.py` s'y laissait prendre — il cherche la chaîne `vault_*` dans les
|
||
fichiers, sans pouvoir savoir qu'un défaut n'est jamais lu. La « source unique » avait donc
|
||
hérité de l'erreur, et le panneau réclamait un secret impossible à fournir avant que l'AC
|
||
n'existe.
|
||
|
||
Le défaut mort est retiré, et la clé disparaît d'elle-même du recensement (25 → 24 exigés),
|
||
du gabarit et du panneau. `client_pki_ca_fingerprint_override` reste le moyen documenté
|
||
d'épingler une empreinte (AC externe, migration).
|
||
|
||
### Corrigé — une troisième copie manuelle de la liste des secrets
|
||
`docs/intrants-communs.md` §H énumérait les secrets à la main, avec les deux mêmes erreurs.
|
||
Elle renvoie désormais à `scripts/voute.py lister` et à la preuve P18 plutôt que d'entretenir
|
||
une copie de plus.
|
||
|
||
## 2026-08-01 (suite 13) — le rappel des secrets dérive de `voute.py`
|
||
|
||
`SECRETS_ATTENDUS` était une liste écrite à la main dans `inventory_gui.py`, en parallèle du
|
||
recensement que `scripts/voute.py` fait déjà depuis le plan, les rôles des groupes actifs et
|
||
les `group_vars`. Deux sources pour la même vérité, et la manuelle avait divergé :
|
||
|
||
| Clé | Panneau | Gabarit | Référencée |
|
||
|---|---|---|---|
|
||
| `vault_ldap_sssd` | annoncée | absente | **nulle part** — aucun rôle `sssd` n'existe |
|
||
| `vault_step_ca_fingerprint` | **omise** | présente | `roles/client_pki/defaults/main.yml:24` |
|
||
|
||
Un opérateur qui suivait le panneau créait donc un secret que rien ne consomme, et oubliait
|
||
celui dont `client_pki` a besoin pour vérifier l'empreinte de l'AC racine.
|
||
|
||
Le rappel dérive désormais de `voute.secrets_exiges()` — la **même source que la preuve
|
||
P18** — augmentée de `SECRETS_HORS_MOTIF` pour les jetons Proxmox, qui ne portent pas le
|
||
préfixe `vault_`. Vérifié : **27 noms, écart nul avec le gabarit**. Sur un dépôt public nu,
|
||
la liste est vide plutôt qu'en erreur.
|
||
|
||
## 2026-08-01 (suite 12) — la fabric se règle depuis la console
|
||
|
||
Tout le modèle d'underlay bâti aujourd'hui — routeur, spanning-tree, fabrics, transit —
|
||
s'éditait à la main dans un YAML, pendant que la doctrine dit qu'un sysadmin doit exploiter
|
||
l'outil **sans IA**. La frontière avait eu sa section dans le panneau ; l'underlay, non.
|
||
|
||
Une section **Fabric** couvre désormais les valeurs plates : le switch routeur, le dialecte
|
||
de CLI, le mode et la topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`,
|
||
et donc les ports) restent hors de portée du panneau — elles demandent une vue dédiée, comme
|
||
celle des serveurs.
|
||
|
||
### Le dialecte devient un intrant déclaré
|
||
Il ne vivait que dans `SETOPS_DIALECTE`, une variable d'environnement. C'est une propriété du
|
||
**matériel**, donc de la fabric : elle se déclare dans `underlay.yml`. Précédence désormais
|
||
explicite : drapeau `--dialecte` > variable d'environnement > intrant déclaré > `cisco`.
|
||
`make underlay` refuse un dialecte inconnu.
|
||
|
||
### Écriture chirurgicale, pas de `safe_dump`
|
||
`underlay.yml` porte 23 lignes de commentaires qui expliquent des décisions d'architecture —
|
||
pourquoi un seul routeur, pourquoi le transit vit dans l'underlay, pourquoi les rayons ne
|
||
sont pas des ports de bord. Un `safe_dump` les aurait toutes effacées, comme c'est arrivé aux
|
||
commentaires de `plan/applications.yml`. Le panneau remplace donc la ligne existante en
|
||
respectant son indentation. Vérifié : trois valeurs modifiées, **73 lignes et 23 commentaires
|
||
avant comme après**.
|
||
|
||
Une clef absente du fichier n'est pas créée : le panneau refuse explicitement plutôt que de
|
||
l'inventer à un endroit arbitraire.
|
||
|
||
## 2026-08-01 (suite 11) — les ports physiques entrent dans le modèle
|
||
|
||
Les noms de ports n'étaient modélisés **nulle part** : `<PORT-VERS-PROXMOX>` et consorts
|
||
étaient des marqueurs littéraux dans le générateur. L'opérateur les remplaçait à la main dans
|
||
la sortie, et recommençait **à chaque régénération**. C'était le seul endroit du devis où le
|
||
travail était perdu à répétition.
|
||
|
||
Ils se déclarent désormais par équipement dans `underlay.yml`, sous quatre clefs qui
|
||
correspondent aux quatre natures de lien :
|
||
|
||
```yaml
|
||
ports:
|
||
hyperviseurs: [Te1/0/1, Te1/0/2, Te1/0/3] # ports terminaux (portfast)
|
||
frontiere: [Gi1/0/23] # vers le pare-feu (portfast)
|
||
rayons: { sleipnir-02: Te1/0/47, … } # côté ROUTEUR
|
||
montante: Te1/0/48 # côté SWITCH D'ACCÈS
|
||
```
|
||
|
||
Le devis émet alors les vrais ports, y compris **plusieurs** vers les hyperviseurs — il n'en
|
||
supposait qu'un seul jusqu'ici, alors que le cluster en compte trois. Non déclarés, les
|
||
marqueurs reviennent : la dégradation est par équipement, un switch renseigné et un autre non
|
||
cohabitent sans problème.
|
||
|
||
La partie B devient **par switch** : adresse de gestion, montante et ports terminaux
|
||
différant d'une machine à l'autre, un bloc commun n'avait plus de sens.
|
||
|
||
`make underlay` refuse un port déclaré deux fois sur un même équipement, un rayon vers un
|
||
switch inconnu, des `rayons` sur autre chose que le routeur, une `montante` sur le routeur
|
||
lui-même. Les quatre cas exercés.
|
||
|
||
## 2026-08-01 (suite 10) — une interface, un bloc
|
||
|
||
La partie A déclarait ses ports de bord deux fois : l'interface en section 4, son `portfast`
|
||
en section 6. La partie B, elle, posait tout dans le même bloc. Deux conventions pour la même
|
||
chose dans un seul document — un opérateur qui applique la partie A section par section
|
||
configurait la même interface à deux endroits.
|
||
|
||
Le `portfast` est désormais posé **avec son interface** (sections 4 et 4b). La section 6 se
|
||
réduit à ce qui est global — mode, priorité du pont racine — plus les deux avertissements :
|
||
que les rayons de la section 4c n'en sont volontairement pas, et pourquoi BPDU guard n'est
|
||
pas émis.
|
||
|
||
Vérifié : aucune interface n'est déclarée deux fois dans une même partie, trois `portfast`
|
||
avec `stp`, zéro sans lui, zéro sur un rayon.
|
||
|
||
## 2026-08-01 (suite 9) — rayons de l'étoile séparés des ports terminaux
|
||
|
||
L'ajout du spanning-tree venait de rendre dangereuse une imprécision qui, jusque-là, n'était
|
||
qu'un titre approximatif. La section 4 s'appelait « Trunk vers Proxmox **+ inter-switch** »
|
||
et n'émettait qu'un port, que la section 6 déclarait en bord de réseau. Réutiliser ce
|
||
placeholder pour les rayons vers les switches d'accès revenait à mettre `portfast` sur les
|
||
liens qui portent les BPDU — c'est-à-dire à désactiver la protection anti-boucle exactement
|
||
là où elle sert.
|
||
|
||
Les rayons sont désormais **dérivés** et émis à part (section 4c côté routeur, B3a côté
|
||
accès), un par switch d'accès, avec l'avertissement qu'ils ne sont pas des ports de bord.
|
||
La section 4 ne désigne plus que les hyperviseurs, et la partie B distingue sa montante
|
||
(`B3a`) de son trunk terminal (`B3b`).
|
||
|
||
`underlay.switches_acces()` devient la source unique du « qui est un switch d'accès » —
|
||
utilisée pour leur devis **et** pour les rayons côté routeur : les deux ne peuvent pas
|
||
diverger.
|
||
|
||
Vérifié : trois commandes `portfast` émises avec `stp` déclaré, **aucune** sans lui, et
|
||
**aucune** sur un rayon.
|
||
|
||
## 2026-08-01 (suite 8) — spanning-tree dérivé de la topologie déclarée
|
||
|
||
### Ajouté — `underlay.stp` et les sections 6 / B4
|
||
Le devis ne disait rien du spanning-tree. Sur une fabric convergée à trois switches, une
|
||
boucle par brassage accidentel n'est pas discrète : c'est une tempête de diffusion.
|
||
|
||
La topologie se déclare (`mode: rstp`, `topologie: etoile`) et le devis en tire la
|
||
configuration. Le **routeur est désigné pont racine** — il est le centre de l'étoile, tous
|
||
les chemins passent déjà par lui, donc l'arbre logique suit le câblage physique au lieu de
|
||
sortir d'une élection arbitraire. Les switches d'accès reçoivent une priorité volontairement
|
||
haute : ils ne doivent jamais devenir racine.
|
||
|
||
Les ports terminaux (hyperviseurs, frontière) sont déclarés en bord de réseau. **BPDU guard
|
||
n'est délibérément pas émis** : un pont Linux dont le STP serait activé enverrait des BPDU et
|
||
ferait tomber le port côté hyperviseur. Le devis dit pourquoi, et à quelle condition
|
||
l'ajouter.
|
||
|
||
En étoile, aucun lien n'est redondant : RSTP est un filet, pas une nécessité — le devis le
|
||
dit plutôt que de laisser croire à une protection indispensable.
|
||
|
||
`make underlay` valide `mode` et `topologie`, et refuse un `stp` déclaré sans `routeur` :
|
||
sans lui, aucun pont racine ne peut être désigné. Sans `stp`, la section signale l'absence
|
||
de protection au lieu de disparaître.
|
||
|
||
Réserve consignée : la forme `binardat` des lignes de spanning-tree n'a pas été confrontée au
|
||
matériel, comme les `ip route`.
|
||
|
||
## 2026-08-01 (suite 7) — l'underlay connaît ses fabrics physiques
|
||
|
||
Le stockage jumbo (iSCSI, Ceph) est porté par un **réseau indépendant de deux switches
|
||
10G**, sans câble commun avec la fabric convergée des `sleipnir`. Le modèle l'ignorait :
|
||
le devis déclarait les VLAN 20/30/31 sur les switches convergés et les mettait dans leurs
|
||
trunks. C'était faux.
|
||
|
||
Chaque réseau de l'underlay porte désormais une `fabric` (`principal` par défaut). Le devis
|
||
ne configure que celle du routeur, et **énonce ce qu'il ne couvre pas** plutôt que de le
|
||
taire :
|
||
|
||
```
|
||
! HORS PERIMETRE — fabric 'stockage' : stockage-iscsi (VLAN 20), ceph-public (VLAN 30), …
|
||
! Portee par des switches distincts, sans cable commun avec celle-ci :
|
||
! ni VLAN a declarer ici, ni trunk, ni spanning-tree partage.
|
||
```
|
||
|
||
Conséquence directe : la question du spanning-tree ne se pose que sur la fabric principale —
|
||
trois switches — et pas sur le stockage, dont les deux switches forment un domaine séparé.
|
||
|
||
Le roster d'hôtes de la section 0 est filtré de la même façon : un équipement d'une autre
|
||
fabric n'apparaît pas, même en commentaire. Un devis est une configuration qu'on applique,
|
||
pas un inventaire. Vérifié en déclarant un hôte de la fabric stockage — il reste absent, et
|
||
la sortie du jour est inchangée.
|
||
|
||
Les `deny` de l'ACL couvrent en revanche **toutes** les fabrics, y compris celles hors
|
||
périmètre : la règle porte sur l'adresse de destination, pas sur le câblage. Si un chemin
|
||
s'ouvre un jour vers le stockage, il est déjà fermé.
|
||
|
||
## 2026-08-01 (suite 6) — nommage : `bifrost` aux frontières, `sleipnir` à la fabric
|
||
|
||
`bifrost-1` et `bifrost-2` sont réservés aux deux frontières OPNsense — Bifröst est le pont
|
||
vers l'extérieur. Les switches internes deviennent `sleipnir-01…03` : le cheval qui traverse
|
||
les mondes, pas le pont qui en sort. La division du nom suit celle de l'architecture.
|
||
|
||
Les deux boîtiers sont déclarés comme hôtes du lien de transit (`10.0.4.1`, `10.0.4.2`) :
|
||
hors flotte Ansible, la déclaration documente le lien et réserve les noms. Le `/29` choisi
|
||
plus tôt les loge tous les deux, comme prévu.
|
||
|
||
Le plan du `/29` est réorganisé en conséquence : frontières en bas (`.1`, `.2`), SVI du
|
||
switch en haut (`.6`), et `.3` laissée libre pour une future IP virtuelle CARP si les deux
|
||
OPNsense passent en haute disponibilité. Ce jour-là, `passerelle_sortie` pointera sur la VIP
|
||
plutôt que sur un boîtier nommé — un seul endroit à changer.
|
||
|
||
Conséquence à traiter : la partie B du devis switch aurait listé les deux pare-feux parmi les
|
||
« switches d'accès ». Elle ne retient désormais que les hôtes du **réseau de management** —
|
||
un équipement déclaré ailleurs ne reçoit aucune ligne de configuration de switch.
|
||
|
||
Le devis frontière nomme le boîtier quand il est déclaré : `frontiere 10.0.4.1 (bifrost-1)`.
|
||
|
||
## 2026-08-01 (suite 5) — deux incohérences du devis switch
|
||
|
||
### Corrigé — le routeur avait deux adresses de gestion contradictoires
|
||
`bifrost-01` portait le SVI `Vlan10 → 10.0.0.1` **et** était déclaré dans `underlay.hotes`
|
||
à `10.0.0.2`. Une interface VLAN n'a qu'une adresse primaire : les deux ne pouvaient pas
|
||
être vraies. L'entrée datait d'avant la désignation du routeur, quand `10.0.0.1` était une
|
||
passerelle abstraite.
|
||
|
||
Le routeur est désormais déclaré à l'adresse du SVI qu'il porte, et `make underlay` **refuse**
|
||
la divergence : `ip 10.0.0.2 != passerelle 10.0.0.1 — le routeur porte ce SVI, les deux
|
||
doivent coïncider`. Le roster des trois switches reste complet.
|
||
|
||
### Corrigé — VLAN de transit déclaré sur les switches d'accès
|
||
La partie B créait `vlan 40` alors que le trunk B3 ne le transporte pas — le transit ne relie
|
||
que le routeur à la frontière. Un VLAN qui n'aurait jamais vu de trame. Il est exclu de la
|
||
partie B, comme il l'est déjà des trunks généraux.
|
||
|
||
## 2026-08-01 (suite 4) — un seul switch route, les autres en L2 pur
|
||
|
||
### Décidé — `underlay.routeur`
|
||
Sans MLAG, le routage est porté par un **unique** switch (`bifrost-01` chez Chezlepro) ; les
|
||
autres restent en L2 pur. Le devis émettait jusqu'ici un jeu unique de SVI sans dire à quel
|
||
switch il s'adressait : appliqué sur les trois, il aurait créé autant de conflits d'adresses
|
||
qu'il y a de zones.
|
||
|
||
### Ajouté — le devis se scinde en deux parties
|
||
- **Partie A — switch routeur** : VLANs, SVI, ACL d'isolation, trunks, routes. Va sur lui
|
||
seul, et l'en-tête le nomme.
|
||
- **Partie B — switches d'accès (L2 pur)** : mêmes VLANs pour commuter les trames étiquetées,
|
||
une adresse de gestion par switch (tirée de `underlay.hotes`) avec `ip default-gateway`
|
||
vers le routeur, et les trunks. Aucun SVI de zone, aucune ACL, aucune route.
|
||
|
||
Deux gardes : `make underlay` refuse un `routeur` qui ne nomme aucun hôte déclaré ; et la
|
||
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine — les coller tous
|
||
écraserait l'adresse. Sans `routeur` désigné, l'en-tête signale explicitement le risque de
|
||
duplication au lieu de laisser croire que le devis est applicable partout.
|
||
|
||
Reste ouvert et consigné : la syntaxe des `ip route` n'est pas dialecte-consciente,
|
||
contrairement aux ACL.
|
||
|
||
## 2026-08-01 (suite 3) — les tenants n'atteignent plus l'underlay
|
||
|
||
### Corrigé — ACL de switch : `deny` vers la fabric physique
|
||
L'ACL d'isolation bloquait l'autre tenant, puis se terminait par `permit ip <tenant> any`.
|
||
Ce `any` autorisait `10.27.x` → `10.0.0.0/24` : le management des switches, celui de Proxmox
|
||
et l'**OOB/IPMI**, plus les réseaux iSCSI et Ceph. Une VM compromise atteignait la console
|
||
physique des hyperviseurs.
|
||
|
||
Le commentaire du générateur disait « Reste → passerelle OPNsense », mais c'est faux pour
|
||
l'underlay : ce trafic est routé **localement** par le switch et ne passe jamais par la
|
||
frontière, donc il n'est jamais filtré par elle.
|
||
|
||
`devis_reseau.py` émet désormais un `deny` par sous-réseau underlay avant le `permit` final,
|
||
dérivé de `underlay.yml` — dialecte respecté (masque normal ou wildcard). Aucun flux du
|
||
registre ne vise l'underlay : le blocage ne casse rien de déclaré.
|
||
|
||
Deux limites consignées dans `docs/frontiere-opnsense.md` §6 : le registre des flux n'a pas
|
||
de mot-clé `underlay`, donc un besoin légitime (superviser l'hyperviseur) ne pourrait pas
|
||
être déclaré ; et `devis_reseau.py` émet un jeu unique de SVI pour trois switches sans MLAG,
|
||
ce qui reste une décision d'architecture ouverte.
|
||
|
||
## 2026-08-01 (suite 2) — le port vers la frontière, et l'ordre d'application
|
||
|
||
### Ajouté — section 4b : le port du switch vers la frontière
|
||
Le devis étiquetait le VLAN de transit sur le trunk `<PORT-VERS-PROXMOX>` et n'émettait
|
||
aucune interface vers le pare-feu. Deux erreurs en une : les hyperviseurs n'ont pas
|
||
d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN tenants — il route vers
|
||
eux, il ne les étiquette pas. Le VLAN de transit sort donc du trunk Proxmox et prend son
|
||
propre port, dérivé du transit déclaré.
|
||
|
||
### Ajouté — avertissement d'ordre en tête de la section 5
|
||
Les routes de la section 5 déplacent la sortie du switch, **y compris celle de ses propres
|
||
réponses**. Tant que l'adresse de la frontière ne répond pas, elles coupent l'accès
|
||
d'administration au switch lui-même — le mécanisme qui a rendu une VM muette le 2026-07-29,
|
||
appliqué cette fois à l'équipement depuis lequel on travaille.
|
||
|
||
Le devis crachait ces lignes à la suite des autres comme si elles étaient équivalentes.
|
||
Il énonce désormais les préalables (boîtier câblé, adressé, joignable depuis le switch,
|
||
session console ouverte) — inacceptable autrement pour un outil censé être exploitable sans
|
||
IA.
|
||
|
||
## 2026-08-01 (suite) — le prochain saut dérive du transit
|
||
|
||
`opnsense_prochain_saut` n'est plus un intrant du panneau : il **dérive** du réseau de
|
||
transit de l'underlay (la `passerelle` du réseau portant `passerelle_sortie`).
|
||
|
||
Un seul bloc de six lignes alimente désormais les deux devis : `10.0.4.1` devient le SVI
|
||
côté switch **et** le prochain saut des routes tenants côté frontière ; `10.0.4.2` devient
|
||
la route par défaut du switch. Le saisir en doublon dans le panneau rouvrait la possibilité
|
||
de deux valeurs contradictoires pour un seul lien — précisément le mode de panne qu'on venait
|
||
de fermer pour les réseaux d'administration.
|
||
|
||
Le devis frontière gagne au passage un bloc `transit` (JSON compris) et cesse de réclamer en
|
||
section 0 des routes que `make devis-reseau` émet maintenant : il dit lesquelles sont **déjà
|
||
émises**, ou signale l'absence de transit déclaré. Sans underlay, le marqueur
|
||
`<PROCHAIN-SAUT-SWITCH>` revient — le repli reste explicite.
|
||
|
||
Reste un seul intrant à figer au câblage : `opnsense_if_transit`, le nom de l'interface qui
|
||
porte le VLAN 40 sur le boîtier — la seule valeur que rien ne peut deviner.
|
||
|
||
## 2026-08-01 — le lien de transit et les deux routes (la boucle est fermée)
|
||
|
||
### Ajouté — réseau de transit dans l'underlay (clé `passerelle_sortie`)
|
||
Le lien entre le routeur est-ouest (switches L3) et la frontière nord/sud manquait dans
|
||
**tous** les fichiers : le devis switch ne contenait pas une seule `ip route`.
|
||
|
||
Il vit dans l'**underlay** et non dans un tenant, pour une raison qui tranche : la frontière
|
||
route vers tous les supernets tenants par le **même** prochain saut. Le lien est donc partagé
|
||
et ne peut dériver d'aucun `index`. Un réseau underlay portant `passerelle_sortie` (l'adresse
|
||
du pare-feu sur le lien) le déclare — chez Chezlepro : VLAN 40, `10.0.4.0/29`, SVI `10.0.4.1`,
|
||
frontière `10.0.4.2`. Un `/29` et non un `/30` parce que deux pare-feux cohabitent pendant la
|
||
transition.
|
||
|
||
`underlay.py` valide la nouvelle clé (sortie sur le lien, distincte du SVI, SVI obligatoire,
|
||
un seul transit) — **preuve P23**, cas de rejet exercés un par un.
|
||
|
||
### Ajouté — `devis-reseau` émet les routes (section 5)
|
||
Deux routes dérivées, et il en faut impérativement deux :
|
||
- **l'aller** : `ip route 0.0.0.0 0.0.0.0 <sortie>` — sans elle, aucun hôte n'a de sortie ;
|
||
- **le retour** : une route par réseau d'administration — sans elle, la réponse d'une VM
|
||
revient au pare-feu par une autre interface que celle où l'état a été créé, et se fait
|
||
jeter en silence. C'est le piège qui a coûté la passe de déploiement du 2026-07-29.
|
||
|
||
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` : **même source
|
||
unique** que la garde anti-lockout des nftables et l'alias `SETOPS_ADMIN` de la frontière —
|
||
les trois pare-feux et les routes ne peuvent pas diverger. Sans transit déclaré, la section
|
||
s'affiche en clair comme manquante plutôt que de disparaître silencieusement.
|
||
|
||
### Corrigé — le panneau refusait d'enregistrer les intrants de la frontière
|
||
`group_vars/opnsense.yml` porte à la fois des paramètres anodins et deux **références** de
|
||
voûte (`{{ vault_opnsense_api_key }}`). La fusion « préserve les clés non gérées » les
|
||
relisait, et le garde-fou, qui ne regardait que les **noms**, les prenait pour des secrets
|
||
soumis. Il regarde désormais la **valeur** : une référence de voûte est un pointeur et passe ;
|
||
toute valeur réelle sur ces clés fait toujours échouer l'écriture. Ajout d'un refus explicite
|
||
à l'entrée pour une requête forgée, au lieu d'un abandon silencieux.
|
||
|
||
### Retiré — reliquat `proxmox.vault.yml`
|
||
La voûte est **unique** (`group_vars/all/vault.yml`) ; l'ancien fichier séparé n'était plus
|
||
chargé automatiquement (aucun groupe `proxmox` dans l'inventaire) et entretenait la confusion.
|
||
Supprimé de l'instance, avec son gabarit.
|
||
|
||
Au passage : `supprimer_vm_debian.yml` ne chargeait **que** ce reliquat pour ses secrets. Le
|
||
supprimer tel quel aurait cassé `make detruire`, l'outil même du rebuild from-zero. Sa liste
|
||
est alignée sur celle du playbook de clonage (`all/vault.yml` en dernier, il l'emporte), et la
|
||
résolution du jeton depuis la voûte unique est vérifiée en exécution réelle.
|
||
|
||
## 2026-07-29 (suite) — la frontière OPNsense, dérivée du registre des flux
|
||
|
||
### Ajouté — `make devis-opnsense` (+ preuve P24)
|
||
La bordure nord/sud devient un **artefact dérivé**, comme le devis switch. Rien de saisi à la
|
||
main : ni port, ni adresse, ni nom d'hôte n'apparaît dans le générateur.
|
||
|
||
Le constat qui rend la chose évidente : `resoudre_flux.py` **saute volontairement** les flux
|
||
`pair: externe` (`scripts/resoudre_flux.py:184`) parce qu'ils ne concernent pas le pare-feu
|
||
d'hôte. Plusieurs `raison` du registre disent déjà « filtré à l'OPNsense ». **La politique de
|
||
la frontière était donc déjà écrite** — il ne restait qu'à la dériver.
|
||
|
||
- **`scripts/devis_opnsense.py`** — agrège les flux `externe`, résout les destinations depuis
|
||
l'inventaire (hôtes actifs **et** planifiés : la frontière se prépare avant les VM), les
|
||
supernets depuis les nomenclatures fédérées, et l'accès d'administration depuis l'intrant
|
||
`nftables_admin_ssh`. Sort un devis relisible ou `--json` (destiné à l'API OPNsense).
|
||
- **Garde anti-lockout (P24)** — `--verifier` **refuse** un devis dont `nftables_admin_ssh`
|
||
est vide : la règle SSH entrante n'aurait aucune source et le `block in` final fermerait
|
||
l'accès d'administration. Même intrant que les nftables d'hôte : source unique, donc pas de
|
||
divergence possible entre la bordure et les hôtes.
|
||
- **Section 0 du devis : les routes de retour à poser sur les switches.** C'est le piège qui a
|
||
coûté la passe de déploiement du jour — la passerelle de zone répond au ping, mais aucun
|
||
hôte derrière elle n'est joignable, parce que la réponse revient au pare-feu par une autre
|
||
interface que celle où l'état a été créé.
|
||
- **`docs/frontiere-opnsense.md`** — les décisions d'architecture (frontière nord/sud, les SVI
|
||
restent sur les switches L3), le partage des rôles entre les trois pare-feux, le chemin
|
||
d'application par l'API, et ce qui reste ouvert (câblage, second VPN, sortie générale).
|
||
|
||
### Corrigé — intrant `nftables_admin_ssh` vide sur l'instance Chezlepro
|
||
Il valait `[]` alors que `group_vars/hotes_actifs.yml` active `nftables_baseline_enabled`.
|
||
Autrement dit : la flotte se serait mise en `policy drop` sans **aucune** règle autorisant le
|
||
contrôleur, qui arrive par le VPN hors du sous-réseau de la flotte. Renseigné à
|
||
`192.168.255.0/24`. Le sous-réseau du second VPN (OPNsense) devra y être ajouté.
|
||
|
||
### Ajouté — la frontière devient réglable depuis la console (section *Frontière*)
|
||
Les valeurs non sensibles du pare-feu de bordure sont de **vrais intrants**, pas un fichier
|
||
YAML tenu à la main : un opérateur règle la frontière depuis le panneau « Intrants de base »,
|
||
sans IA et sans éditeur.
|
||
|
||
- **`INTRANTS_SCHEMA`** — nouvelle section *Frontière* : `opnsense_api_url`,
|
||
`opnsense_api_verifier_certs`, `opnsense_if_wan`, `opnsense_if_transit`,
|
||
`opnsense_prochain_saut`. Cible d'écriture `group_vars/opnsense.yml`, en **fusion** (comme
|
||
Proxmox) pour préserver les références de voûte du fichier. Le panneau rend ses sections
|
||
génériquement : aucune modification d'interface n'a été nécessaire.
|
||
- **Garde-fou** — `opnsense_api_key` / `opnsense_api_secret` ajoutés à
|
||
`INTRANTS_CLES_INTERDITES` : le GUI **refuse** de les écrire, donc impossible de coller un
|
||
secret d'API dans le panneau par mégarde. Ils n'apparaissent qu'en lecture seule, par leur
|
||
nom, dans `SECRETS_ATTENDUS`.
|
||
- **`devis_opnsense.py`** lit désormais ces intrants et n'affiche ses marqueurs
|
||
(`<IF-TRANSIT>`, `<PROCHAIN-SAUT-SWITCH>`) qu'en repli : le devis se complète de lui-même
|
||
dès que la console est renseignée.
|
||
|
||
### Ajouté — les identifiants d'API de la frontière, dans la voûte
|
||
Même partage que Proxmox : l'anodin en clair, le secret dans la voûte unique de l'instance.
|
||
|
||
- **`group_vars/opnsense.yml`** (instance, en clair) — URL de gestion, interfaces et prochain
|
||
saut à figer au câblage, plus les **références par nom** `{{ vault_opnsense_api_key }}` et
|
||
`{{ vault_opnsense_api_secret }}`. Aucune valeur de secret n'y figure.
|
||
- **Gabarit de voûte** — les deux clés ajoutées à `vault.yml.example`. `scripts/voute.py` les
|
||
a recensées **tout seul** depuis les `group_vars` (il ne lit jamais la voûte, il ne compare
|
||
que des noms) : le gabarit passe de 23 à 25 secrets, et la **preuve P18** reste verte.
|
||
|
||
Validation : `make verifier` vert — **24 preuves CONFORME**, 0 échec, 0 sauté.
|
||
|
||
## 2026-07-29 — dette documentaire soldée (un README par rôle) + carte revue
|
||
|
||
### Ajouté — les 12 README de rôles manquants
|
||
**Tous les rôles ont désormais un README.** Les 12 restants sont écrits, au format maison
|
||
(intention → rôle → variables → notes/limites → prérequis), et documentent surtout ce qui
|
||
ne se lit pas dans les tâches :
|
||
|
||
- **Socle et résolution** — `serveur_debian` (rôle-catégorie sans tâches : il ne porte que le
|
||
flux SSH du plan de gestion, *sans quoi les nftables couperaient l'accès Ansible*),
|
||
`hosts_statiques` (le plancher `/etc/hosts` qui rend l'ordre de reconstruction possible).
|
||
- **Rôles utilitaires** — `resoudre_base` et `resoudre_annuaire` : entrées/sorties (facts),
|
||
et *pourquoi* le FQDN plutôt que le nom court (fédération + `verify-full`).
|
||
- **Courriel** — `serveur_dovecot` (les trois réglages Dovecot 2.4 qui conditionnent la
|
||
remise ; le local-part seul comme chemin commun LMTP/IMAP), `serveur_postfix` (liens
|
||
`mailstore`/`milter`, recopie de `/etc/hosts` dans le chroot), `serveur_rspamd` (clé DKIM
|
||
idempotente ; le domaine signé doit être **public** en prod).
|
||
- **Sauvegardes** — `client_backup` (jobs déclaratifs, chiffrement côté client, *le dépôt
|
||
neuf est vide tant qu'une première sauvegarde n'a pas tourné*) et `serveur_backup`
|
||
(hors-nœud ≠ hors-site).
|
||
- **SSO et supervision** — `serveur_oauth2_proxy` (le patron réutilisable Keycloak-devant-
|
||
n'importe-quoi ; pourquoi `allow_unverified_email` est nécessaire avec un annuaire LDAP)
|
||
et `serveur_icingaweb2` (modes `ldap` vs `external`, et l'écoute à restreindre en SSO).
|
||
- **`client_unbound`** — le garde-fou de bascule du résolveur (`apply` **et** `confirm`,
|
||
validation avant de toucher `/etc/resolv.conf`).
|
||
|
||
### Corrigé — `docs/carte-set-ops.md` ne décrivait plus l'état du code
|
||
- **`expose` est consommé au déploiement** (la carte l'annonçait encore comme « Phase 3 à
|
||
venir ») : vhosts nginx dérivés via `expositions_des_applications` +
|
||
`expositions.conf.j2`, alias `/etc/hosts`, SANs d'edge dérivés par `instancier.py`.
|
||
- **Trois mécanismes transverses ajoutés au catalogue** : résolution d'annuaire
|
||
(`resoudre_annuaire`), plancher de résolution (`hosts_statiques`), et `resoudre_base`
|
||
nommé dans la ligne des bindings app→base.
|
||
- **Cinq entrées d'index ajoutées** : réseau/pare-feu, ordre de déploiement, preuve/recette,
|
||
exploitation courante, wiki — des pans entiers du corpus n'étaient pas indexés.
|
||
- Reste ouvert, explicitement : `meta/liens.yml` sur le seul `serveur_postfix`, et `requiert`
|
||
non consommé au déploiement (indice applicatif ; l'ordre, lui, vient des couches+graphe).
|
||
|
||
Validation : `make verifier` vert (ansible-lint 514 fichiers, tests, syntax-check,
|
||
**23 preuves CONFORME**).
|
||
|
||
## 2026-07-28 — figures annotées dans le wiki (console d'exploitation)
|
||
|
||
### Ajouté — les 8 vues de la console illustrées, dans le wiki
|
||
La série des figures annotées (une par vue du GUI, en **SVG auto-contenu** : capture + repères
|
||
intégrés en base64) est désormais **intégrée** à l'unité wiki
|
||
[`Le GUI (console d'exploitation)`](wiki/Le-GUI-console-d-exploitation.md), en fin de section ②.
|
||
Vues éditables (Serveurs, Applications, Bases × 2, Domaines) puis dérivées (Flux, Couches, Réseau).
|
||
|
||
- **Symlink `wiki/img → ../docs/img`** — foyer unique des figures dans `docs/img/` ; les pages wiki
|
||
y réfèrent en relatif (`img/*.svg`) sans duplication dans l'arbre source.
|
||
- **`make wiki-publier`** embarque désormais les `docs/img/*-annote.svg` (déréférencés) dans le
|
||
wiki Forgejo publié — c'étaient jusqu'ici les seules `.md` qui voyageaient, donc aucune image.
|
||
|
||
## 2026-07-24 — underlay (fabric physique, cluster-global)
|
||
|
||
### Ajouté — l'underlay comme concept de premier plan
|
||
Le modèle dérive l'adressage **par tenant** (VLAN `1000+index×10+zone`), mais la **fabric
|
||
physique** qui porte la flotte — mgmt des switches, mgmt Proxmox/OOB, iSCSI, Ceph — n'appartient
|
||
à aucun tenant et ne dérive d'aucun `index`. Elle vit dans le *sous-sol* du modèle. Jusqu'ici
|
||
elle n'était pas codifiée. Elle l'est.
|
||
|
||
- **`scripts/underlay.py`** + **`make underlay`** — charge/affiche/**valide** `underlay.yml` :
|
||
réseaux (nom, VLAN, sous-réseau, passerelle optionnelle → SVI, MTU/jumbo) et hôtes fixes
|
||
documentés (les switches). La validation refuse toute **collision avec la plage tenant** :
|
||
VLAN < 1000, sous-réseaux hors des supernets `10.(10+index).0.0/16`.
|
||
- **`underlay.yml`** (racine du moteur, **gitignore** comme le vault ; gabarit public
|
||
`underlay.yml.example`), surchargeable par `SETOPS_UNDERLAY`. Absent → tout reste inchangé.
|
||
- **`make devis-reseau`** émet désormais une **section 0. Underlay** (VLANs, SVI, hints jumbo,
|
||
IP des switches en commentaire) et ajoute les VLAN underlay au **trunk** Proxmox, avant les
|
||
tenants. Respecte le dialecte (`cisco`/`binardat`).
|
||
- **Preuve P23** — `underlay.py --verifier` : la fabric n'empiète pas sur la plage tenant.
|
||
**Sautée** (⚪) si `underlay.yml` est absent (dépôt public), comme P16 sans vault.
|
||
|
||
Le plafond tenant (245) est inchangé : l'underlay occupe `10.0.0.0/16 .. 10.10.0.0/16`, laissé
|
||
libre par la dérivation (index ≥ 1 → `10.11+`).
|
||
|
||
## 2026-07-23 (suite 7)
|
||
|
||
### Ajouté — dialecte de CLI du commutateur (`devis-reseau`)
|
||
Constat de l'opérateur : son switch est un **Binardat**, dont la CLI diffère de Cisco sur
|
||
deux points que le devis généré ignorait — et deux pièges qui font passer un VLAN mais fuir
|
||
un tenant :
|
||
|
||
- **Masque d'ACL** — Cisco veut un masque **inversé** (wildcard `0.0.255.255`), Binardat un
|
||
masque **normal** (`255.255.0.0`). Le SVI (`ip address … 255.255.255.0`) était déjà normal,
|
||
donc valide sur les deux.
|
||
- **`remark`** — Binardat n'a pas la commande de commentaire d'ACL de Cisco. Ces lignes
|
||
faisaient rejeter le bloc.
|
||
|
||
**`scripts/devis_reseau.py`** gagne un **dialecte** (`cisco` par défaut, `binardat`) :
|
||
`masque_acl()` choisit wildcard ou masque normal ; `remarque()` omet les `remark` en Binardat.
|
||
Réglable par `--dialecte`, par `SETOPS_DIALECTE`, ou `make devis-reseau DIALECTE=binardat`. Le
|
||
GUI (lecture seule) suit l'env. Le code public reste **générique** (défaut `cisco`).
|
||
|
||
## 2026-07-23 (suite 6)
|
||
|
||
### Ajouté — plan de recette (le pendant manuel de `make prouver`)
|
||
Constat de l'opérateur : les 78 exercices « ④ À toi de jouer » du wiki forment, ensemble, un
|
||
**plan de tests d'acceptation**. Formalisé, sans dupliquer :
|
||
|
||
- **`scripts/plan_recette.py`** + **`make plan-recette`** — **génère** `docs/audit/plan-de-recette.md`
|
||
depuis les exercices du wiki : une grille auto-contenue (le geste inline) par unité, colonnes
|
||
*Ce qu'on éprouve · Le geste · Type (observe/casse-répare) · **Preuve auto***. La dernière colonne
|
||
est extraite du texte (le `Pxx` que l'exercice mentionne) : elle montre quels gestes manuels sont
|
||
**aussi** gardés par la machine. Étant générée, la grille **ne peut pas dériver** du wiki.
|
||
- **Preuve P22** — `plan_recette.py --verifier` échoue si le fichier committé n'est plus à jour
|
||
(le wiki a changé sans régénérer). Le plan de recette devient un artefact **auto-gardé**.
|
||
- **Honnêteté de couverture** assumée dans le document : un « — » = **manuel seul** (aucune preuve
|
||
machine ne le double) ; le plan ne prétend pas à l'exhaustivité au-delà des exercices du wiki.
|
||
|
||
C'est le **pendant humain** de `make prouver` : le harnais prouve le *moteur* (P01–P21), la recette
|
||
valide l'*exploitation* — et sert de checklist au `protocole-operateur-independant.md` (« exploitable
|
||
sans IA »).
|
||
|
||
Validé : 78 gestes sur 19 unités, 5 doublés d'une preuve `Pxx` ; P22 testée (détecte une dérive) ;
|
||
`make verifier` → **CONFORME 22/22** (contre une instance cohérente).
|
||
|
||
## 2026-07-23 (suite 5)
|
||
|
||
### Ajouté — wiki : l'axe « méthode » (KB enrichie)
|
||
Le wiki enseignait les fondamentaux *services* (identité, PKI, courriel…) mais pas la
|
||
*méthode* de Set-OPS. Cinq pages ajoutées, au moule à 4 temps (concept → Set-OPS →
|
||
transférable → à toi de jouer), avec exercices concrets :
|
||
|
||
- **Le plan & l'adressage dérivé** — un seed (`index`), tout en découle (DRY, source unique).
|
||
- **Multi-instance & fédération** — un moteur, N écosystèmes ; découverte par convention.
|
||
- **La preuve** — « ne jamais affirmer plus que ce qu'on prouve » ; le registre, `make prouver`,
|
||
P01–P21.
|
||
- **Le GUI (console d'exploitation)** — éditer la source, dry-run avant apply, l'invalide
|
||
impossible à saisir (le `<select>` sans hôte fantôme) ; la flotte et la bascule.
|
||
- **Glossaire** — 24 concepts en une phrase (était « à venir »).
|
||
|
||
Raccordées dans `Home.md` (deux unités-pilotes : services **et** méthode) et `_Sidebar.md`
|
||
(section « Flotte & preuve »). Fidèle à la doctrine : le wiki pointe vers `docs/`, ne recopie
|
||
pas. Publié via `make wiki-publier`. Wiki : 855 → 1573 lignes, 22 pages, aucun lien mort.
|
||
|
||
## 2026-07-23 (suite 4)
|
||
|
||
### Ajouté — créer un MODÈLE (`make model-creer`, dépôt privé)
|
||
Symétrique de `instance-creer`, mais produit un modèle réutilisable dans le dépôt PRIVÉ
|
||
(`Set-OPS-Modeles/`, jamais `exemples/modeles/`). `scripts/model_creer.py`, deux modes :
|
||
|
||
- **`MODE=base BASE=<modele> NOM=<x>`** — copie un modèle déjà générique (copier + éditer).
|
||
- **`MODE=instance SOURCE=OPS-<x> NOM=<y>`** — **promeut une instance éprouvée en modèle** :
|
||
généralise l'identité (`domaine → exemple.internal`, organisation → `Exemple`, realm →
|
||
`exemple`), fixe `index → 1`, `setops_production → false`, `nftables_admin_ssh → []`, vide
|
||
la clé publique de sauvegarde, générique `proxmox.yml` (host/nœud/stockage/VMID vidés,
|
||
golden template → `modele-debian13`).
|
||
|
||
Sûreté : **aucun secret** ne sort (`vault.yml`/`proxmox.vault.yml` jamais copiés ; refus si
|
||
l'un subsiste ; le `.example` est conservé). Copie **ciblée** (plan/ + inventories/ seulement,
|
||
symlinks résolus) — robuste aux dépôts imbriqués et boucles de symlinks. Le modèle produit
|
||
**doit valider** (`modeles.py verifier`), sinon il est annulé. `make model-creer`.
|
||
|
||
Validé : les deux modes testés en isolement (destination tmp, dépôt privé jamais touché) —
|
||
promotion du labo (cohérent) réussie + validée, aucun `chezlepro` résiduel, aucun secret,
|
||
`proxmox.yml` génériqué ; copie base (socle) OK ; refus d'une instance INVALIDE (le garde-fou
|
||
a détecté un hôte fantôme dans une instance en cours d'édition). `node --check`, `make verifier`
|
||
rc=0 CONFORME 21/21.
|
||
|
||
## 2026-07-23 (suite 3)
|
||
|
||
### Ajouté — créer une instance depuis un modèle (CLI + GUI)
|
||
Le moteur est indépendant des instances (le symlink `instance/` est gitignoré, aucun
|
||
artefact d'instance n'est committé). Créer une instance existait seulement à la main
|
||
(`cp -r` + `ln -s`, QUICKSTART). C'est désormais une capacité de premier ordre.
|
||
|
||
- **`scripts/instance_creer.py`** — copie un modèle (`exemples/modeles/*` + `SETOPS_MODELES`)
|
||
vers un dépôt frère `../<nom>`, y fixe l'`index` (le seed), et **refuse** : un nom déjà
|
||
existant (rien n'est écrasé), un modèle inconnu, un **index en collision** avec une
|
||
instance fédérée (le garde-fou vérifie AVANT toute copie). Le `.git` et
|
||
`hosts.genere.yml` du modèle ne sont pas copiés.
|
||
- **`make instance-creer NOM=OPS-X MODELE=socle [INDEX=N]`** et **`make instance-modeles`**
|
||
(liste les modèles + les index déjà pris).
|
||
- **GUI, vue Réseau** — formulaire « Créer une instance depuis un modèle » sous la flotte :
|
||
modèle (liste), nom, index. `POST /api/instance-creer` ; `/api/instances` renvoie aussi
|
||
`modeles` et `index_pris`. Ne bascule pas l'active (geste explicite).
|
||
|
||
### Corrigé
|
||
- **Gabarit de voûte du labo complété.** P18 (voûte) a échoué en passant l'active sur le
|
||
labo : son `vault.yml.example` était resté à l'ancienne version (17 clés, sans
|
||
`vault_restic_password`, `vault_oauth2_cookie`, etc.). Aligné sur le gabarit complet
|
||
(23 secrets). P18 fait exactement son travail — attraper un gabarit incomplet, quelle que
|
||
soit l'instance active.
|
||
|
||
Validé : garde-fous de création testés en isolement (collision d'index refusée avant copie,
|
||
écrasement refusé, modèle inconnu refusé, aucune pollution des dépôts frères) ; `node --check`
|
||
du GUI ; `make verifier` rc=0 **CONFORME 21/21** (active = labo).
|
||
|
||
## 2026-07-23 (suite 2)
|
||
|
||
### Ajouté — bascule d'instance depuis le GUI (vraiment multi-instance)
|
||
Demande explicite et répétée de l'opérateur : gérer les instances **depuis le GUI**, pas
|
||
seulement en CLI. Le plan de contrôle reste maison, mais cette capacité y entre.
|
||
|
||
- **Inventaire résolu dynamiquement.** Le serveur GUI figeait l'inventaire au démarrage
|
||
(`args.inventaire.resolve()`). Il est désormais relu **à chaque requête** depuis le
|
||
symlink `instance/` (propriété `Gestionnaire.inventaire`) — la bascule prend effet sans
|
||
redémarrer. Les chemins de plan (`FICHIER_*`) étaient déjà relatifs au symlink et suivent
|
||
de même. `SETOPS_INVENTAIRE` force encore un inventaire fixe (CI).
|
||
- **`POST /api/instance-utiliser`** + `basculer_instance(nom)` — repointe le symlink avec
|
||
les garde-fous de `make instance-utiliser`, plus une **validation stricte** : `nom` doit
|
||
être une instance **découverte** (dossier frère), ce qui interdit toute traversée de
|
||
chemin (testé : `../etc` refusé).
|
||
- **GUI, vue Réseau** — la table de flotte gagne un bouton **« Activer »** par instance
|
||
(l'active affiche « active »). Il confirme (avec avertissement renforcé si l'instance est
|
||
en **production**), bascule, puis recharge la page : toutes les vues et les déploiements
|
||
visent la nouvelle instance.
|
||
|
||
L'édition du drapeau `federe` reste au CLI (rarement changé). La bascule, elle, est
|
||
maintenant CLI **et** GUI.
|
||
|
||
Validé : `node --check` du GUI ; bascule testée de bout en bout (repoint → l'inventaire
|
||
dynamique suit → traversée refusée → restauration) ; `make verifier` rc=0 CONFORME 21/21.
|
||
|
||
## 2026-07-23 (suite)
|
||
|
||
### Ajouté — gestion multi-instances : vue d'ensemble + garde-fou de collision
|
||
Le mécanisme de bascule existait déjà (`make instance-utiliser`, `make instance-courante` :
|
||
repointer le symlink `instance`). Ce qui manquait : le regard d'ensemble et le filet.
|
||
|
||
- **`make instances`** (`scripts/instances.py`) — liste toutes les instances de la
|
||
fédération (dépôts frères avec `plan/nomenclature.yml`), marque l'active (`*`), et
|
||
montre pour chacune index, plage VLAN dérivée, statut fédéré/local (`federe`) et
|
||
production (`setops_production`). Lecture seule.
|
||
- **Détection de collision d'index** — signale toute paire d'instances **fédérées**
|
||
partageant un `index` (donc mêmes VLAN/VMID sur le trunk convergé). C'est exactement le
|
||
piège vécu (Chezlepro-prod et le labo tous deux à l'index 1) : désormais crié, pas
|
||
découvert par hasard. Le mode `--verifier` sort en erreur (rc=2) sur collision.
|
||
- **Preuve P21** (`make prouver`/`make verifier`) — câble ce garde-fou dans le harnais :
|
||
la cohérence de la fédération est vérifiée à chaque passage. No-op quand moins de deux
|
||
instances fédérées sont présentes (comme P17 sans `SETOPS_MODELES`).
|
||
|
||
Validé : `make instances` liste les 3 instances (labo LOCAL, Technolibre et Chezlepro
|
||
fédérées, index 2 et 13) ; détection testée en synthétique (deux fédérées au même index →
|
||
collision levée ; labo `federe: false` → cohérent) ; `make verifier` rc=0 **CONFORME 21/21**.
|
||
|
||
## 2026-07-23
|
||
|
||
### Changé — l'adressage se dérive du seul seed `index` (RUPTURE, mode compact retiré)
|
||
Principe posé par l'utilisateur : les valeurs de configuration doivent se dériver des
|
||
intrants, pas se réécrire à la main. La nomenclature dupliquait ce que `index` détermine
|
||
déjà (supernet, sous-réseaux, passerelles, VLAN). C'est corrigé, en rupture nette.
|
||
|
||
- **`inventory_rules`** : nouvelles fonctions de dérivation, **source unique** —
|
||
`supernet_de`, `base3_de`, `sous_reseau_de`, `passerelle_de`, `vlan_de`. Le modèle à
|
||
6 zones est encodé une fois : 2ᵉ octet = `10+index`, 3ᵉ octet de zone = `15+catégorie`,
|
||
VLAN trunk = `1000+index×10+zone`. `deriver_nomenclature` ne lit plus **aucun** adressage
|
||
stocké ; le mode `compact` est supprimé (tout est ip-miroir dérivé).
|
||
- **`devis_reseau`** importe ces helpers (plus de duplication) ; découverte des tenants sur
|
||
`index` présent (le filtre `vmid_schema` disparaît).
|
||
- **Les 10 nomenclatures** (3 instances + 7 modèles) passent au **format maigre** : `index`,
|
||
`cidr_hote`, `reservations`, libellés de zones et `fonctions` seulement. Supprimés :
|
||
`supernet`, `vmid_schema`, et par zone `sous_reseau`/`passerelle`/`vlan`. `presence-web`,
|
||
encore en `compact`, gagne `index: 1`.
|
||
- **GUI** : `index` devient un **intrant** (section « Réseau » du panneau Intrants). Il vit
|
||
dans la nomenclature (le plan réseau, uniforme partout — contrairement aux intrants des
|
||
modèles, hétérogènes) et le GUI l'écrit **chirurgicalement** (une ligne, sans reformater
|
||
le fichier). Le miroir JS dérive le VLAN du seed (fin de la lecture de `c.vlan` stocké).
|
||
|
||
### Ajouté
|
||
- **Preuve P20** (`preuve_nomenclature_derivee`) : aucune nomenclature ne stocke d'adressage
|
||
— garde-fou permanent contre une rechute vers l'écriture manuelle. Testée en négatif
|
||
(une rechute simulée est bien rejetée).
|
||
|
||
Validé : **DIFF VIDE sur les 3 instances** (la dérivation reproduit exactement l'adressage
|
||
qui était stocké), les **7 modèles valident**, `devis_reseau` génère les mêmes VLAN
|
||
(1011-1016 dérivés), `make verifier` rc=0 **CONFORME 20/20**, `node --check` du GUI OK,
|
||
aller-retour d'écriture de `index` : une seule ligne touchée.
|
||
|
||
## 2026-07-22 (suite)
|
||
|
||
### Ajouté — trois preuves qui ferment les angles morts du harnais
|
||
Le constat : `make prouver` ne vérifiait qu'*une* instance et le seul modèle `socle`. Tout
|
||
ce qui vit à côté du moteur dérivait en silence — d'où l'hôte fantôme d'`integral`, les
|
||
neuf secrets absents du gabarit Chezlepro, les champs de plan que le GUI ne sait pas écrire.
|
||
|
||
- **P17 — tous les modèles valident** (`scripts/modeles.py`). Rejoue les 4 validateurs de
|
||
registres sur chaque modèle découvert (les `exemples/modeles/*` du dépôt, plus les chemins
|
||
de `SETOPS_MODELES`, ex. le dépôt privé). **A immédiatement trouvé 6 modèles invalides
|
||
sur 7** : cinq portaient `autorite: interne` (valeur périmée jamais propagée depuis la
|
||
correction de `socle` en phase 5), `presence-web` manquait son domaine interne. Corrigés.
|
||
- **P18 — gabarit de voûte complet** (`scripts/voute.py`). Confronte `vault.yml.example`
|
||
aux secrets réellement exigés par le plan (champ `secret` des bases + références `vault_*`
|
||
des rôles actifs et des group_vars). Ne déchiffre jamais la vraie voûte : compare des noms.
|
||
`--strict` signale aussi les clés devenues inutiles.
|
||
- **P19 — le GUI couvre le plan** (`scripts/couverture_gui.py`). Croise les champs présents
|
||
dans les plans réels (instance + modèles) avec `CHAMPS_ECRITS_PAR_GUI`, nouveau tableau de
|
||
`inventory_gui.py` déclarant ce que les fonctions de sauvegarde écrivent. Signale tout
|
||
champ non éditable par le GUI (**a trouvé `applications.websocket`**, comblé — case à
|
||
cocher ajoutée), et toute dérive entre le tableau et le source du GUI. La nomenclature
|
||
reste un trou connu (lecture seule), tolérable via `--tolerer nomenclature`.
|
||
|
||
### Corrigé
|
||
- **Garde-fou contre l'hôte fantôme** : `valider_applications` accepte désormais le registre
|
||
des serveurs et **refuse une application posée sur un hôte non déclaré** — la faute exacte
|
||
qu'`integral` portait. Câblé dans `scripts/applications.py` (les 4 appels), au POST du GUI,
|
||
et vérifié : re-teste le bug d'origine → rejeté avec un message actionnable.
|
||
- **6 modèles de `Set-OPS-Modeles`** : `autorite: interne` → `auto-heberge` (collaboration,
|
||
forge, identite, integral, observabilite) ; `presence-web` gagne son domaine interne pour
|
||
ses expositions `site.*`/`app.*`.
|
||
- **Champ `websocket` dans le GUI** (inspecteur d'application) — Collabora en a besoin.
|
||
|
||
Validé : `make verifier` → **rc=0, CONFORME 19/16→19** (P01–P19), `ansible-lint` 0 échec,
|
||
les 7 modèles valident (`SETOPS_MODELES` posé), gabarit de voûte complet (23/23), couverture
|
||
GUI 27/27 champs (nomenclature tolérée), DIFF VIDE conservé, JS du GUI valide.
|
||
|
||
## 2026-07-22
|
||
|
||
### Ajouté
|
||
- **Champ « Liens (bindings) » dans le GUI** (`scripts/inventory_gui.py`). L'inspecteur
|
||
d'application porte une section dédiée : une ligne par lien, `rôle → cible`, les deux en
|
||
listes déroulantes, avec bouton d'ajout et de retrait. Les **rôles proposés sont
|
||
exactement ceux que le rôle porteur déclare accepter** (`roles/<groupe>/meta/liens.yml`),
|
||
et les cibles sont les autres applications du plan. Chaque ligne affiche les **variables
|
||
que le lien injectera**. Changer le groupe d'une application vide les rôles de lien
|
||
devenus inacceptés. Comble un manque relevé à l'audit du GUI : les bindings ne pouvaient
|
||
se déclarer qu'en éditant `plan/applications.yml` à la main, ce qui rendait la
|
||
configuration du courriel (Postfix → Dovecot, Postfix → rspamd) inaccessible sans éditeur.
|
||
- **`liens_acceptes(groupe)` et `catalogue_liens()`** dans `scripts/inventory_rules.py` —
|
||
source **unique** de « quels liens un rôle accepte », partagée par le validateur, le GUI
|
||
et `instancier.py`. Nouvelle clé `liens_acceptes` dans la charge utile de `/api/inventaire`.
|
||
|
||
### Modifié
|
||
- **`valider_applications` valide désormais les `liens`** : liste de tables `{vers, role}`,
|
||
champs non vides, cible connue, pas de lien vers soi-même, et **rôle accepté par le rôle
|
||
porteur** (avec la liste des rôles acceptés dans le message d'erreur). Un rôle sans
|
||
`meta/liens.yml` reste toléré au validateur — `instancier.py` tranche avec le même message.
|
||
Bénéficie aussi à `make inventaire-verifier` et à `scripts/applications.py verifier`.
|
||
- **`scripts/instancier.py`** — sa copie locale `_liens_acceptes()` est retirée au profit de
|
||
la fonction partagée. Un seul endroit lit `meta/liens.yml`.
|
||
- **`docs/bindings-conception.md`** — la Phase 4 passe à 🟡 : l'éditeur est fait, le
|
||
**graphe des liens reste à faire**.
|
||
|
||
Validé : `make verifier` → **rc=0**, `ansible-lint` 0 échec sur 485 fichiers,
|
||
`prouver.py --verifier` → CONFORME 16/16, `verifier_gui.py` (node --check) OK,
|
||
**DIFF VIDE** conservé et les deux variables de binding du courriel toujours générées
|
||
(`serveur_postfix_mailstore_hote`, `serveur_postfix_rspamd_milter`). Aller-retour
|
||
d'écriture testé : les liens survivent au cycle chargement → sauvegarde → relecture.
|
||
Sept garde-fous du validateur éprouvés (rôle inconnu, cible inexistante, lien vers
|
||
soi-même, champ manquant, types invalides).
|
||
|
||
## 2026-07-21 (suite)
|
||
|
||
### Modifié
|
||
- **Palette aurore enrichie sur les quatre pages `promo/`** — ajout de deux teintes,
|
||
`--rose:#ff6fc4` (frange magenta) et `--vert:#5cff9d` (vert fluo), présentes uniquement
|
||
dans les **dégradés foncés** : fond fixe du corps, nappe `.aurora` dérivante, et filets
|
||
de séparation `.rule`. Opacités de 5,5 % à 8,5 % — une teinte, pas un motif. Les
|
||
dégradés de texte et de boutons (`--aurora`) sont **inchangés** : l'identité de marque
|
||
ne bouge pas. Appliqué identiquement aux quatre fichiers (0 conflit CSS après coup).
|
||
- **Thème Forgejo étendu au wiki** (`roles/serveur_forgejo/files/custom/public/assets/css/alliance.css`,
|
||
29 → 118 lignes). La feuille étant injectée par `templates/custom/header.tmpl` sur toutes
|
||
les pages, le wiki est couvert **sans feuille distincte**. Réorganisée en deux sections
|
||
de risque explicite : **§1 Variables** (surcharge des variables de couleur officielles,
|
||
sûr, résiste aux mises à jour) et **§2 Décor** (fond aurore, filet sous les titres,
|
||
citations, tableaux et code en ligne de `.markup` — sélecteurs internes, à revérifier à
|
||
chaque montée de version majeure). Supprimer §2 ramène au thème sobre d'origine. Le ciel
|
||
nocturne ne s'applique qu'aux thèmes sombres, pour ne pas casser le thème clair.
|
||
`roles/serveur_forgejo/README.md` documente le tout.
|
||
|
||
### Ajouté
|
||
- **`docs/theme-forgejo-hors-flotte.md`** — pose **manuelle** du thème sur une instance
|
||
Forgejo **non gérée par Set-OPS** (la forge historique qui héberge ce dépôt et son wiki).
|
||
Forgejo n'ayant aucun réglage web pour le CSS, il faut déposer la feuille et référencer
|
||
`header.tmpl` sur le serveur. La procédure **ne duplique aucun fichier** : elle pointe
|
||
vers ceux de `roles/serveur_forgejo/files/custom/`. Comprend la détection du répertoire
|
||
`custom`, un garde-fou pour ne pas écraser un `header.tmpl` existant (ajout de ligne, pas
|
||
remplacement), la vérification par `curl`, et la marche arrière.
|
||
|
||
### Corrigé
|
||
- **État de forge-01 élucidé — ce n'était pas une contradiction.** L'hôte a été **créé,
|
||
puis supprimé**. Le plan dit donc `etat: planifie` (état **courant**) et le CHANGELOG du
|
||
2026-07-03 dit le branding « prouvé sur forge-01 » (état **passé**) : les deux disent
|
||
vrai. `roles/serveur_forgejo/README.md` l'énonce désormais ainsi ; la preuve reste
|
||
valable, simplement non rejouable tant que l'hôte n'est pas recréé. (La note précédente,
|
||
qui présentait l'écart comme une contradiction à trancher, était erronée.)
|
||
- **Teintes rose et verte trop concentrées dans les coins** — nappes élargies d'environ
|
||
×2 (780×540 → 1500×1020 px pour le rose, 840×580 → 1620×1120 px pour le vert), ramenées
|
||
vers l'intérieur (97 % → 76 %, 3 % → 20 % ; nappe dérivante 95 % → 79 % et 6 % → 22 %),
|
||
extinction repoussée de 62 % à 82 % avec un **arrêt intermédiaire** pour une rampe douce,
|
||
et flou de `.aurora` porté de 64 px à 86 px. La couleur est répartie au lieu d'être tassée
|
||
aux bords.
|
||
- **Opacités des teintes réduites d'environ 40 %** dans la foulée — l'étalement les rendait
|
||
trop présentes. Rose `.075 → .044`, vert `.058 → .034`, violet `.062 → .036` (arrêts
|
||
intermédiaires abaissés dans la même proportion), et opacité de la nappe `.aurora`
|
||
`.44 → .32`. Géométrie inchangée : seule l'intensité baisse.
|
||
- **Statut du thème précisé, section par section** : §1 (variables) est **éprouvée** sur
|
||
forge-01 ; §2 (décor), ajoutée aujourd'hui, n'a **jamais été rendue** par un Forgejo réel.
|
||
L'affirmation précédente (« non rendu par un Forgejo réel », tous les cas confondus) était
|
||
fausse pour §1.
|
||
|
||
Validé : équilibre des accolades et parenthèses du CSS, 0 conflit CSS entre les quatre
|
||
pages promo, `ansible-lint` 0 échec, `prouver.py --verifier` → CONFORME 16/16.
|
||
|
||
## 2026-07-21
|
||
|
||
### Ajouté
|
||
- **Protocole d'épreuve de l'opérateur indépendant** (`docs/audit/protocole-operateur-independant.md`).
|
||
Met **AFF-002** (« Set-OPS s'exploite entièrement à la main, sans aucune IA ») à l'épreuve
|
||
d'un sysadmin qui n'est pas l'auteur, sur sa propre grappe Proxmox, à froid depuis le modèle
|
||
public `socle`. Définit : la **règle du silence** (l'observateur journalise, n'aide pas ; trois
|
||
niveaux N0/N1/N2), les **interdits** (aucune IA, aucun accès aux dépôts privés, aucune lecture
|
||
de `docs/audit/` qui divulguerait les pièges connus), les **critères de réussite R1→R6 fixés
|
||
d'avance**, le périmètre matériel et la sécurité, et le **gabarit de rapport**
|
||
(`operateur-independant-AAAA-MM-JJ.md`). Le registre peut **perdre** : le protocole prévoit
|
||
explicitement la redescente d'AFF-002 en 🟡 ou ❌ selon le verdict.
|
||
|
||
### Modifié
|
||
- **`docs/audit/affirmations.md`** — nouvelle limite consignée : **AFF-002 est ✅ *par
|
||
inspection*, pas par démonstration**. Ses écarts bloquants sont soldés, mais aucun opérateur
|
||
indépendant ne l'a exécutée ; ✅ y signifie « plus aucun défaut connu ». Renvoi vers le
|
||
protocole.
|
||
- **`docs/audit/README.md`** — « Les trois pièces » → « Les pièces » : ajout du protocole comme
|
||
quatrième pièce du dispositif (l'épreuve humaine, hors harnais automatisable).
|
||
|
||
Validé : `python3 scripts/prouver.py --verifier` → **CONFORME 16/16** (voûte exportée) ;
|
||
équilibre des blocs de code du nouveau document vérifié. Aucun code touché.
|
||
|
||
## 2026-07-20
|
||
|
||
### Modifié
|
||
- **`make verifier` inclut désormais les preuves (`make prouver`).** `make verifier` se
|
||
termine par `python3 scripts/prouver.py --verifier` : nouveau mode qui exécute toutes les
|
||
preuves du registre (verdict + code de sortie) **sans écrire de rapport**, pour ne pas
|
||
écraser la pièce justificative committée `docs/audit/preuve-<date>.md`. `make verifier`
|
||
échoue donc si une preuve échoue. `make prouver` seul (sans `--verifier`) continue d'écrire
|
||
le rapport horodaté. Vérifié : `make verifier` → CONFORME 16/16 (voûte exportée), aucun
|
||
churn du rapport committé ; `make prouver` écrit toujours. Doc mise à jour
|
||
(`docs/audit/README.md` § « Rapport avec `make verifier` »).
|
||
- **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)
|
||
|
||
### Modifié
|
||
- **Nomenclature `ip-miroir` : longueurs UNIFORMES.** Le VLAN dérivé passe à
|
||
`1000 + index×10 + zone` → **toujours 4 chiffres** (1011..4094), donc le VMID
|
||
(`VLAN·octet·seq`) fait **toujours 9 chiffres**. Longueurs uniformes et mnémotechniques
|
||
(retirer 1000 redonne `index×10+zone`). Les deux tenants sont **convergés** sur le réseau
|
||
fédéré 10/8 : Chezlepro (idx 1) → VLANs 1011-1016 / 10.11.x ; Technolibre (idx 2) →
|
||
VLANs 1021-1026 / 10.12.x. Chezlepro quitte le bac-à-sable plat 192.168.15/VLAN 15.
|
||
|
||
### Ajouté
|
||
- **`make devis-reseau` : config switch dérivée du plan.** Nouveau `scripts/devis_reseau.py`
|
||
qui découvre les instances fédérées (`../*/plan/nomenclature.yml`, schéma `ip-miroir`) et
|
||
émet la config Cisco-like du réseau convergé — **VLANs + SVIs (passerelles) + ACLs d'isolation
|
||
inter-tenant** — dérivée des nomenclatures, jamais saisie à la main (toujours synchrone avec
|
||
le plan). One-shot précédemment ; maintenant reproductible.
|
||
- **GUI : vue « Réseau » (devis switch) + bouton Copier.** Nouvel onglet lecture seule qui
|
||
affiche le devis `make devis-reseau` (VLANs + SVIs + ACLs), via `GET /api/devis-reseau`, avec
|
||
un bouton **Copier** (prêt à coller sur le switch). Un opérateur génère et transmet la config
|
||
sans CLI.
|
||
- **GUI : erreurs de déploiement en LANGAGE CLAIR.** Quand une action (créer/vérifier/déployer)
|
||
échoue, le GUI n'oblige plus à lire le dump Ansible : un encadré résume les tâches en erreur —
|
||
**étape · hôte · type · message clair** — avec un bouton **« Copier »** (pour transmettre le
|
||
résumé à un humain / au mainteneur). Le backend (`_extraire_echec`) parse les `fatal:`/`UNREACHABLE!`,
|
||
privilégie la vraie cause (`stderr` plutôt que le générique « non-zero return code »), gère
|
||
`no_log` (« sortie masquée — secret ») et les injoignables. Éprouvé sur les 4 échecs réels du
|
||
from-zero du jour. `node --check` OK.
|
||
- **GUI : éditeur « Domaines » (6ᵉ onglet éditable).** Le registre des domaines publics — le seul
|
||
sans édition dans le GUI — a désormais son onglet : ajouter/retirer une zone DNS, **autorité**
|
||
(sélecteur `primaire-cache`/`auto-heberge`/`delegue`), **edge**, **DNSSEC**, **mail**, secondaires,
|
||
et affichage des **expositions (FQDN) sous la zone**. Backend : route `/api/domaines`,
|
||
`ecrire_domaines`, validation `valider_domaines` (rejette une autorité inconnue). Éprouvé :
|
||
round-trip backend OK, `node --check` OK, smoke serveur OK. (Le `autorite: interne` périmé des
|
||
instances Chezlepro/Technolibre a été corrigé en `auto-heberge` au passage.)
|
||
- **GUI : persistance des journaux d'exécution.** Chaque action lancée depuis l'interface
|
||
(créer-vm / vérifier / déployer) écrit désormais, **en plus du streaming live**, un journal
|
||
horodaté `<instance>/logs/<hôte>-<action>-<date>.log` (gitignoré). Bonus : si le navigateur se
|
||
déconnecte, l'exécution **continue** et le journal est capturé jusqu'au bout (au lieu d'être
|
||
interrompue) — utile pour un déploiement qu'on ne veut pas voir avorter à la fermeture d'un onglet.
|
||
- **GUI : vues « Flux » et « Couches » (lecture seule).** Deux nouveaux onglets rendent visibles
|
||
dans l'interface deux registres jusque-là en ligne de commande seulement : **Flux** = la matrice
|
||
d'audit réseau (rôle · sens · port · pair · chiffrement coloré · raison, depuis les `meta/flux.yml`),
|
||
et **Couches** = l'ordre de reconstruction (socle → pki → services → apps → agents, avec les groupes
|
||
triés topologiquement par couche). Alimentées par `resoudre_flux`/`orchestrer` via l'API. Éprouvé :
|
||
`node --check` OK, smoke test serveur (63 flux, 6 couches servis).
|
||
|
||
### Modifié
|
||
- **GUI : intégration des intrants de session.** Le panneau « Intrants de base » expose désormais
|
||
**`nftables_admin_ssh`** (type liste, section « Sécurité ») — la garde anti-lockout du pare-feu
|
||
était éditable en fichier mais absente du GUI. Retrait de `vault_step_ca_fingerprint` des secrets
|
||
attendus (empreinte du root CA désormais dérivée dynamiquement, plus un secret). `node --check` OK.
|
||
|
||
## 2026-07-07 — Reconstruction from-zero PROUVÉE (preuve de portabilité « sans réserve »)
|
||
|
||
Les 14 VM du lab (+ sauvegardes + AC) supprimées, puis `make myDay` a reconstruit l'écosystème
|
||
POC **de rien** : 13 hôtes déployés (0 échec), et `make valider` **entièrement vert** (Prometheus
|
||
6/6 UP, 6 vhosts HTTPS, courriel bout-en-bout remis, 6 dépôts restic restaurés) — le tout sous
|
||
pare-feu actif. 4 bugs de portabilité débusqués et corrigés dans le moteur :
|
||
|
||
### Corrigé
|
||
- **client_pki : empreinte du root CA dérivée dynamiquement** (au lieu d'une valeur figée en Vault).
|
||
Une AC régénérée (from-zero) a une empreinte neuve ; le rôle la lit désormais de l'autorité
|
||
elle-même (`step certificate fingerprint`, délégué au nœud step-ca), source de vérité.
|
||
`client_pki_ca_fingerprint_override` permet un épinglage explicite.
|
||
- **serveur_keycloak : assignation de rôle tolérante aux utilisateurs absents.** Sur un annuaire
|
||
vide (from-zero), assigner un rôle à un user inexistant échouait ; on vérifie désormais son
|
||
existence (Keycloak fédère LDAP à la demande) et on saute proprement sinon.
|
||
- **serveur_prometheus : ne scrute que les hôtes ACTIFS** (`client_metrique ∩ hotes_actifs`). Un
|
||
hôte planifié (non déployé) n'est plus une cible morte.
|
||
- **Pare-feu nftables compatible Docker.** Le ruleset résolu (a) remplace **uniquement la table
|
||
`setops_flux`** au lieu de `flush ruleset` (préserve les tables Docker : DNAT/forward des
|
||
conteneurs) et (b) autorise `docker0` + `ct established,related` dans la chaîne `forward`. Sans
|
||
ça, `forward policy drop` coupait Collabora (conteneur). Diagnostic prouvé au niveau paquet.
|
||
|
||
### Ajouté
|
||
- **`playbooks/proxmox/supprimer_vm_debian.yml`** — suppression de VM par VMID (from-zero), avec
|
||
garde-fous : n'agit que sur les VMID présents dans le cluster, refuse si le modèle est ciblé,
|
||
secrets `no_log`.
|
||
|
||
## 2026-07-07
|
||
|
||
### Ajouté
|
||
- **`make valider` — recette d'acceptation fonctionnelle (phase 4, v1).** Vérifie que les services
|
||
*fonctionnent*, pas juste qu'ils sont déployés (complète les `*-verifier` statiques). `playbooks/valider.yml`,
|
||
lecture seule : **cibles Prometheus toutes UP** (API `/targets`) + **vhosts HTTPS exposés répondent**
|
||
(dérivés des `server_name` réels de l'edge, filtrés sur `domaine_interne` — générique) + **courriel
|
||
bout-en-bout** (envoi via le MTA → LDAP → LMTP → Maildir, remise vérifiée par `doveadm` sur le
|
||
compte `testmail`, message de test nettoyé) + **restauration de sauvegarde** (pour chaque nœud
|
||
`client_backup` : restic restaure le dernier snapshot dans un dossier temporaire — lecture seule
|
||
sur le dépôt — et vérifie que des fichiers en sortent). Éprouvé sur la flotte vivante sous pare-feu
|
||
actif : 7 cibles UP, 8 vhosts OK, courriel remis, 6 dépôts restaurables. **La recette a trouvé un
|
||
vrai trou** (collab-01/Nextcloud sans `client_backup_jobs` → service de sauvegarde en échec, fichiers
|
||
non protégés), corrigé côté instance.
|
||
- **Pare-feu nftables activé sur TOUTE la flotte (14 nœuds, activation prudente).** Les 14 hôtes
|
||
actifs tournent sous `policy drop` avec leur ruleset **résolu moindre-privilège** (flux est-ouest
|
||
déclarés autorisés par source, reste refusé). Vérifié en conditions réelles : flux déclarés OPEN
|
||
(keycloak→pg, postfix→dovecot LMTP, prometheus→node_exporter…), flux non déclaré DROP
|
||
(forge→redis), 14/14 `active`+`enabled`+`policy drop`, contrôleur toujours joignable.
|
||
- **Intrant `nftables_admin_ssh`** (garde anti-lockout) — CIDR d'administration TOUJOURS
|
||
autorisés en SSH, indépendamment des flux. Le résolveur (`resoudre_flux.py`) l'injecte en tête
|
||
de chaque ruleset. **Bug de conception rattrapé avant activation** : le contrôleur Ansible arrive
|
||
par VPN (`192.168.255.2`, hors sous-réseau flotte) — sans cette règle, activer = lockout immédiat.
|
||
- **Rollout prudent** : d'abord `infra-pki-01` seul (test dead-man switch `systemd-run`, SSH
|
||
re-vérifié sous drop, puis permanent), puis les 13 autres par lot (dead-man 5 min + sonde des
|
||
flux est-ouest avant de persister). Activation pilotée par `nftables_baseline_enabled: true`
|
||
(group_vars `hotes_actifs` ; le golden template n'y est pas → reste sans pare-feu, voulu).
|
||
- Le déploiement dépose `instance/flux-genere/<hôte>.nft` dans `/etc/nftables.conf` + service
|
||
`enabled` (survit reboot ET futurs `make myDay`).
|
||
|
||
### Corrigé
|
||
- **Détection du coffre Vault : `production/` codé en dur → inventaire réel.** `deployer`,
|
||
`deployer-tout` et `verifier-deploiement` cherchaient le coffre chiffré dans
|
||
`inventories/production/group_vars`, alors qu'une instance en `principal/` (cas courant) n'a pas
|
||
ce chemin → l'invite du mot de passe Vault ne se déclenchait jamais et le déploiement échouait au
|
||
déchiffrement. Corrigé : la garde vise désormais le `group_vars` de l'inventaire **résolu**
|
||
(`$(dir $(INVENTAIRE_PRODUCTION))group_vars`). Vérifié : le coffre de `principal/` est bien détecté.
|
||
|
||
### Modifié
|
||
- **`nftables_baseline` branché sur les flux résolus (reconstruction, phase 0).** Le rôle déploie
|
||
désormais le ruleset **résolu** généré par `make flux` (`instance/flux-genere/<hôte>.nft` — règles
|
||
par source, `ip saddr` = moindre privilège) quand il est présent ; sinon repli sur le gabarit plat.
|
||
**Toujours `nftables_baseline_enabled: false` par défaut → aucune activation** (l'activation reste
|
||
un geste dédié, testé par nœud). Nouveau var `nftables_baseline_ruleset_genere`. Syntax-check OK.
|
||
### Ajouté
|
||
- **Reconstruction from-zero en une commande (reconstruction, phase 3 — outillage).** La création
|
||
de VM était unitaire (`creer-vm HOTE=…`) ; on comble le trou entre *créer* (2a) et *configurer*
|
||
(2b) :
|
||
- **`make flotte-creer CONFIRMER=true`** — boucle `creer-vm` sur tous les hôtes actifs du plan
|
||
(clone Proxmox). Nouvelle sous-commande `inventory_host.py lister-actifs`.
|
||
- **`make reconstruire CONFIRMER=true`** — enchaîne **flotte-creer → attente SSH de la flotte
|
||
(`_attendre-flotte`, `ATTENTE_MAX` réglable) → `deployer-tout`**. La reconstruction complète en
|
||
une commande, **idempotente de bout en bout** : le clone (`proxmox_kvm`) saute une VM déjà
|
||
présente (par nom), le réseau/disque sont `present`/`resized` (grow-only), le déploiement Ansible
|
||
converge. Re-lançable sans risque, qu'il reste des VM ou non.
|
||
- **`make myDay`** repointé sur `reconstruire` (le vrai « bouton rouge » ; n'était qu'un alias de
|
||
`deployer-tout`). Distinction assumée : `deployer-tout` = **converger** la config d'une flotte
|
||
existante (2b, avec `MODE_CHECK=1`) ; `reconstruire`/`myDay` = **créer les VM manquantes puis
|
||
déployer** (2a+2b).
|
||
Gardes `CONFIRMER=true` sur les trois. Non testé contre Proxmox/lab (validé : énumération des 14
|
||
hôtes actifs, refus sans `CONFIRMER`, enchaînement `make -n`).
|
||
|
||
## 2026-07-06
|
||
|
||
### Ajouté
|
||
- **`make wiki-publier` — fin du dernier geste manuel (reconstruction, phase 0).** Le wiki
|
||
pédagogique (`wiki/`, source versionnée) se publie désormais dans le wiki Forgejo par
|
||
`make wiki-publier WIKI_REMOTE=…<dépôt>.wiki.git` : clone superficiel du wiki, synchronisation des
|
||
pages (`wiki/*.md` sauf `README.md` ; suppressions propagées), commit + push seulement s'il y a du
|
||
changement. Refuse sans `WIKI_REMOTE`. Éprouvé de bout en bout contre un dépôt bare local (17
|
||
pages publiées = 17 source, diff vide, README exclu, `_Sidebar` inclus, idempotent au 2e passage).
|
||
|
||
- **Registre des flux réseau complété (reconstruction, phase 0).** Transcription du travail
|
||
zéro-confiance est-ouest dans `meta/flux.yml` : **16 rôles remplis** (step_ca, openldap, powerdns,
|
||
prometheus, loki, redis, rspamd, backup, dovecot, postfix, keycloak, forgejo, grafana, icinga,
|
||
icingaweb2, nextcloud, oauth2_proxy + le socle `serveur_debian` pour le plan de gestion SSH, +
|
||
les 5 clients pki/journal/smtp/backup/unbound). Le registre couvre désormais **29 rôles, 63 flux**
|
||
(qui-parle-à-qui : port, sens, pair, chiffrement, raison) — la base de génération nftables/OPNsense
|
||
et la matrice d'audit. Schéma enrichi (`docs/flux-conception.md`) : valeurs `ssh` (transport SSH,
|
||
restic/backup + SSH de gestion) et `tls-cible` (TLS visé, feuille de route edge→backends).
|
||
**Point critique traité** : SSH (22) déclaré au socle, sinon les nftables générés couperaient
|
||
l'accès Ansible. Validé : schéma conforme (0 erreur) et **matrice cohérente** (tout egress vers un
|
||
service a l'ingress correspondant en face). Reste phase 0 : la cible `make wiki-publier`.
|
||
|
||
- **Résolveur de flux (reconstruction, phase 0 — §Séquence 2).** `scripts/resoudre_flux.py` agrège
|
||
les `meta/flux.yml`, résout les `pair`, et produit **deux artefacts, hors-ligne, sans activation** :
|
||
- **`docs/registre-flux.md`** (généré) — la matrice d'audit source→destination (rôle, sens, port,
|
||
chiffrement, raison) + synthèse chiffrement. Artefact du label de certification.
|
||
- **aperçus nftables par hôte** (`instance/flux-genere/<hôte>.nft`, gitignorés) — règles résolues
|
||
avec IP réelles, `ip saddr` = **moindre privilège** (ex. LMTP 24 sur le mail n'accepte que l'IP
|
||
du nœud Postfix), `policy drop`. **Aperçus inspectables, NON activés** (l'activation reste un
|
||
geste dédié testé par nœud, cf. flux-conception §Activation prudente).
|
||
- Cibles **`make flux`** (registre + aperçus) et **`make flux-verifier`** (schéma + matrice,
|
||
branché dans `make verifier`). Validé : 29 rôles / 63 flux cohérents, 14 aperçus générés.
|
||
Reste : brancher `nftables_baseline` (modèle plat aujourd'hui) sur ces aperçus, et le test lab.
|
||
|
||
- **Orchestrateur ordonné (reconstruction, phase 2).** `playbooks/site.yml` n'est plus un stub :
|
||
c'est désormais un **point d'entrée ordonné généré**, qui déploie l'écosystème **couche par
|
||
couche, dans l'ordre de reconstruction**, sans intervention manuelle. Nouveautés :
|
||
- **`docs/couches-deploiement.yml`** — registre central des couches ordonnées
|
||
(`socle → pki_racine → pki_client → services → apps → agents`), les 30 groupes déployables classés.
|
||
- **`scripts/orchestrer.py`** — trie les groupes par couche (clé primaire) puis **topologiquement
|
||
intra-couche** via `dependances-groupes.yml` (ex. dovecot avant postfix, icingaweb2 après icinga,
|
||
nextcloud en dernier). Génère `site.yml` comme une séquence d'`import_playbook`. Artefact du
|
||
moteur (déterministe, sans donnée d'instance ; Ansible saute les groupes sans hôte actif).
|
||
**Deux gardes anti-dérive** (refus si violé) : bijection univers↔couches (un nouveau rôle non
|
||
classé casse la génération) et aucune arête « en arrière » (un prérequis dans une couche plus
|
||
tardive = classification fausse). Éprouvées par test négatif.
|
||
- **`make site`** (régénère + syntax-check), **`make site-verifier`** (cohérence, branché dans
|
||
`make verifier`), **`make deployer-tout CONFIRMER=true`** (déploiement orchestré de la flotte,
|
||
limité à `hotes_actifs` ; garde `CONFIRMER` car action impactante ; `MODE_CHECK=1` pour l'essai
|
||
idempotent à blanc). Validé : `verifier` OK (30 groupes, aucun cycle/arête arrière),
|
||
`--syntax-check` du `site.yml` généré OK, refus `deployer-tout` sans `CONFIRMER` (rc=2).
|
||
|
||
### Modifié
|
||
- **Audit exhaustif du codé-en-dur (reconstruction, phase 1b).** Balayage complet
|
||
`tasks + templates + defaults` de tous les rôles (noms de tenant, IP, domaines, emails, orgs).
|
||
Résultat : le moteur ne porte plus **aucun** nom de tenant en dur. Corrigé — les labels/slug
|
||
OIDC dérivent désormais de l'intrant `organisation` : `serveur_forgejo_oidc_nom` (slug de
|
||
callback, `organisation | lower | replace(' ','-')`), `serveur_grafana_oidc_nom`,
|
||
`serveur_nextcloud_oidc_nom`, `serveur_nextcloud_theme_nom` (labels d'affichage). Commentaires
|
||
« Se connecter avec Chezlepro » → génériques. Non-régression SSO : `organisation: Chezlepro` →
|
||
slug `chezlepro`, **identique** à l'URI de redirection Keycloak de l'instance (pas de casse).
|
||
Conservés intentionnellement : realm `default('chezlepro')` (décision `identite_realm` actée) et
|
||
le thème visuel **Alliance Boréale** (identité par défaut assumée du réseau, pas un tenant).
|
||
Validé : re-balayage vide, rendu Jinja du slug testé (Chezlepro/Alliance Boréale/Ma Coop),
|
||
`--syntax-check` OK (playbook forgejo via inventaire `principal`).
|
||
- **Audit du graphe de dépendances (reconstruction, phase 1a).** `docs/dependances-groupes.yml`
|
||
gagne les prérequis inter-groupes confirmés dans le code, en vue de l'orchestrateur trié en
|
||
topologie. Ajouts : `serveur_keycloak` → **`serveur_openldap`** (fédération LDAP via
|
||
`resoudre_annuaire_uri`, en plus de PostgreSQL) ; **`serveur_dovecot`** → `serveur_openldap`
|
||
(userdb/passdb LDAP) ; **`serveur_postfix`** → `serveur_dovecot` (remise LMTP au mailstore) ;
|
||
**`serveur_icingaweb2`** → `serveur_icinga` + `serveur_postgresql` + `serveur_openldap`
|
||
(IcingaDB + auth LDAP) ; **`serveur_nextcloud`** → `serveur_postgresql` + `serveur_keycloak`
|
||
(OIDC) + `serveur_collabora` (validation WOPI). Réconciliation `meta/liens.yml` : le seul lien
|
||
structurel (`serveur_postfix` mailstore → Dovecot) coïncide avec le graphe. **Conclusion d'archi :**
|
||
la règle « TLS vérifié ⇒ `client_pki` aux deux bouts » ne devient PAS des arêtes par-groupe
|
||
(client_pki est quasi universel) — c'est une **couche** de l'ordre de reconstruction
|
||
(`socle → step_ca → client_pki → services → apps → agents`) ; `dependances-groupes.yml` ne
|
||
capture que le fin ordonnancement intra-couche. Validé : YAML conforme, **aucun cycle**, tri-topo
|
||
réussi (19 nœuds), chargeur `charger_dependances` accepte (12 groupes, `est_groupe_operationnel`
|
||
OK), tous les groupes ont un rôle.
|
||
|
||
## 2026-07-05
|
||
|
||
### Modifié
|
||
- **VLAN dérivé du tenant (réseau convergé).** `deriver_nomenclature` (schéma `ip-miroir`) dérive
|
||
désormais le VLAN = `index × 10 + zone` — **unique globalement** sur un trunk convergé (chaque
|
||
tenant son bloc de 10 ; 1-9 réservés à l'infra partagée). Le VLAN contenant déjà le tenant (1er
|
||
chiffre = index), le VMID **mène avec le VLAN** (`VLAN·octet·seq`, ≤ 9 chiffres Proxmox). Ex.
|
||
Technolibre (index 2) → VLANs 21-26, VMID 2101101. Le schéma `compact` (lab, sandbox) reste
|
||
inchangé. Champs `vlan:` codés en dur retirés des catégories ip-miroir (désormais dérivés).
|
||
|
||
### Ajouté
|
||
- **Zéro-confiance est-ouest — flux Métriques, Logs et Courriel chiffrés.** Suite du chantier
|
||
(après PostgreSQL) : **métriques** (node_exporter sert en HTTPS via cert step-ca +
|
||
`--web.config.file` + cert-sync owned prometheus ; Prometheus scrape `scheme: https` +
|
||
`tls_config`), **logs** (Loki `http_tls_config` + cert-sync owned loki ; Alloy push `https` +
|
||
`tls_config`), **courriel** (LMTP `edge-mta→infra-mail:24` en `lmtp_tls_security_level=verify` +
|
||
`lmtp_tls_CAfile` ; `client_smtp` en STARTTLS vérifié). Chacun prouvé de bout en bout (200 HTTPS,
|
||
cibles UP, livraison `status=sent`, HTTP rejeté). Motif cert-sync `.path` industrialisé. Reste :
|
||
edge→backends + DNS (DoT).
|
||
- **VMID 9 chiffres mnémotechnique (schéma `ip-miroir`, opt-in).** `vmid_schema: ip-miroir` dans la
|
||
nomenclature → VMID `I·VVV·HHH·NN` (index·VLAN·octet-hôte·séquence) : le VMID *contient* l'IP
|
||
(`10.(10+index).VLAN.hôte`) + le tenant, lisible d'un coup d'œil. Défaut `compact` rétro-compatible
|
||
(instances déployées inchangées).
|
||
- **Instance partenaire Technolibre.** Écosystème complet (12 VM, `etat: planifie`) dans
|
||
`10.12.16.0/20`, **6 zones de sécurité** (Frontière/Identité/Données/Services-infra/Observabilité/
|
||
Applications, un /24 + VLAN chacune), index de fédération 2, VMID ip-miroir. Preuve de portabilité
|
||
d'un tenant.
|
||
|
||
### Modifié
|
||
- **Références par FQDN partout (fin des IP codées en dur).** Décision d'archi : FQDN pour toute
|
||
référence inter-services (non ambigu en fédération, canonique pour TLS ; nom court = hostname OS).
|
||
`resoudre_base` renvoie le FQDN (→ keycloak/forgejo/icinga) ; `client_journal_loki_url` dérivé du
|
||
groupe `serveur_loki` ; defaults db_host IP morts nettoyés. **Aucune IP littérale dans les defaults.**
|
||
- **Découplage du tenant d'origine.** Realm SSO centralisé sur l'intrant **`identite_realm`** (défaut
|
||
`chezlepro` ; les 4 rôles keycloak/forgejo/grafana/oauth2_proxy en dérivent ; exposé dans la GUI).
|
||
Vars brandées renommées génériques : `chezlepro_timezone→fuseau_horaire`,
|
||
`chezlepro_organisation→organisation`. Le moteur ne porte plus le nom d'un tenant.
|
||
- **Modèles d'instance rafraîchis.** `integral` régénéré depuis le cas prouvé (6 zones, fonctions
|
||
éprouvées `data-sql`/`id-ldap`/`id-sso`/`sup`, ip-miroir, nouveaux intrants) ; `socle`/`identite`/
|
||
`observabilite`/`forge` réalignés sur le même moule (prouvés : dérivation + `valider_serveurs`).
|
||
`presence-web` marqué **aspirationnel** (rôles web-frontal/dorsal absents) plutôt que faussement prêt.
|
||
|
||
## 2026-07-04
|
||
|
||
### Ajouté
|
||
- **Zéro-confiance : flux PostgreSQL entièrement chiffré et vérifié.** PG sert désormais son **cert
|
||
step-ca** (vérifiable contre root_ca) au lieu du snakeoil, et **refuse** toute connexion non-TLS
|
||
du réseau (`hostssl` dans pg_hba). Les 3 clients passent en **verify-full** : keycloak
|
||
(`db-url-properties sslmode=verify-full`), forgejo (`SSL_MODE=verify-full` + `PGSSLROOTCERT`),
|
||
IcingaDB (`tls: true` + `ca`). Prérequis posés : `client_pki` sur data-sql-01 (cert), et
|
||
**`root_ca.crt` en 0644** (cert public, requis par les clients TLS non-root). cert-sync PG
|
||
(motif `.path`, owned postgres) + reload de l'instance `postgresql@NN-main`. Vars :
|
||
`serveur_postgresql_tls_actif`/`_tls_force`, `serveur_*_db_sslmode`/`_ca`. Prouvé de bout en bout
|
||
(cert Set-OPS CA servi, apps 200, non-TLS rejeté « aucun chiffrement », TLS accepté).
|
||
|
||
### Corrigé
|
||
- **PG : détection de version robuste (collision avec un répertoire non-numérique).** Placer le
|
||
`tls_dir` sous `/etc/postgresql/` faisait choisir `tls` comme « version » de cluster
|
||
(`find | sort | last`) → configs déployées au mauvais endroit (verrou hostssl inopérant).
|
||
Corrigé : détection filtrée aux dossiers **numériques** (`^[0-9]+$`) + `tls_dir` déplacé sous
|
||
`/var/lib/postgresql/tls`.
|
||
- **Renouvellement de cert : recharger le VRAI consommateur (bug latent de flotte).** Le
|
||
`cert-renewer@.service` (client_pki) renouvelait le cert sur disque mais son `ExecStartPost`
|
||
rechargeait un service nommé *d'après le cert* (`%i` = FQDN), inexistant → **nginx (et postfix,
|
||
dovecot, slapd) n'étaient jamais rechargés** et servaient l'**ancien cert jusqu'à expiration**.
|
||
Symptôme vécu : cert edge expiré en mémoire (renouvelé sur disque), échec TLS de l'échange
|
||
code→jeton OIDC → **login Grafana/SSO cassé** (tous les services derrière l'edge). Correctif :
|
||
`client_pki_reload_services` (liste des vrais consommateurs), câblée par groupe (edge→nginx,
|
||
mail→postfix/dovecot, annuaire→slapd). Appliqué + vérifié sur les 4 hôtes. Fix immédiat de
|
||
l'incident : `systemctl reload nginx` sur l'edge.
|
||
|
||
### Ajouté
|
||
- **Doc à jour : unité wiki « Autorisation & RBAC », leçon renouvellement, runbooks.** Fermeture
|
||
des dettes de doc : nouvelle **unité wiki authZ/RBAC** (pendant d'*Identité & SSO*, avec l'exemple
|
||
Grafana), **section « le renouvellement est un système »** versée dans l'unité *PKI* (comparer cert
|
||
servi vs fichier ; recharger le consommateur), et **`docs/runbooks-exploitation.md`** (cert expiré,
|
||
RBAC Grafana, branding Forgejo). 15 unités wiki désormais.
|
||
- **UI des logs Loki : dashboard Grafana provisionné.** Loki n'a pas d'UI ; son UI est Grafana.
|
||
Ajout d'un **dashboard « Journaux de la flotte »** (dossier Set-OPS) : sélecteur d'hôte multi +
|
||
filtre regex insensible à la casse + panneau logs + débit par hôte. Référence Loki par une
|
||
**variable de datasource** (`ds_loki`), pas un UID codé en dur (leçon : ajouter un uid explicite à
|
||
une datasource déjà provisionnée casse le démarrage de Grafana). **Visible par les Viewers** (dont
|
||
testmail) sans accès Explore. Prouvé sur obs-01 (dashboard chargé, Grafana actif).
|
||
- **RBAC via SSO : rôle de realm → niveau Grafana.** Machinerie *additive et idempotente* dans
|
||
`serveur_keycloak` (`rbac-oidc.yml`) : rôles de realm (`serveur_keycloak_realm_roles`), **mapper
|
||
`roles`** sur les clients choisis (`_role_mapper_clients`, rôles de realm → claim `roles` dans
|
||
ID token + userinfo), **assignations** rôle→utilisateur (`_role_assignments`). Côté Grafana,
|
||
`role_attribute_path` (`grafana-admin→Admin`, `grafana-editor→Editor`, sinon Viewer). kcadm **à
|
||
chaud, zéro coupure SSO**. Prouvé (idempotence `changed=0`) sur id-sso-01 : rôles créés, mapper
|
||
présent, `testmail` = `grafana-editor` (→ Explore). Illustre l'**authZ** (vs authN du SSO).
|
||
|
||
## 2026-07-03
|
||
|
||
### Ajouté
|
||
- **Identité visuelle Alliance Boréale sur Forgejo (léger, officiel).** Branding via le dossier
|
||
**`custom/`** de Forgejo (mécanisme *officiel* — pas de fork, résistant aux MAJ) : accent **aurore
|
||
par variables CSS** (`--color-primary`…, aucune classe interne touchée), **logo/favicon étoile**
|
||
(réutilisés du thème Keycloak), **page d'accueil brandée** (`home.tmpl` : hero aurore + accroche),
|
||
**thème sombre** par défaut, **nom + méta**. Codifié dans `serveur_forgejo`
|
||
(`serveur_forgejo_branding`, `_app_name`, `_theme`), déployé dans `{{ data }}/custom/`. **Prouvé**
|
||
sur forge-01 : accueil rend (200, « Forge Chezlepro »), `alliance.css` servi (cyan aurore), lint OK.
|
||
- **Wiki pédagogique Forgejo — 14 unités d'apprentissage.** Set-OPS comme *compagnon pédagogique* :
|
||
chaque service = une lentille sur un fondamental TIC, méthodes génériques (on apprend OIDC, pas
|
||
Keycloak). Source versionnée dans `wiki/`, publiée dans le wiki Forgejo (`eregion`). Moule à 4
|
||
temps (concept → Set-OPS → transférable → à toi de jouer, avec *casse-répare*).
|
||
- **GUI : boutons cohérents.** « Pousser » (surchargé : clonait *et* déployait) → « 🖥 Créer la VM »
|
||
pour le clone, « Déployer » partout pour le déploiement, dry-run obligatoire partout. GUI 100 %
|
||
française.
|
||
- **Agents d'observabilité/ops éprouvés — observabilité *flotte-complète*.** Les 3 intégrations
|
||
« agent » (liaisons nœud × optionnelles) déployées sur 4 nœuds (obs-01, data-sql-01, id-ldap-01,
|
||
forge-01) et **prouvées** : `client_metrique` (node_exporter → **Prometheus scrape les 4 cibles,
|
||
toutes UP**) ; `client_journal` (journald → **Loki reçoit les logs des 4 nœuds**) ; `client_smtp`
|
||
(msmtp → **courriel système d'un nœud relayé par l'edge-MTA et livré**). Comble le trou : les
|
||
*serveurs* d'observabilité (Grafana/Prometheus/Loki) étaient prouvés, mais pas la collecte
|
||
fleet-wide — désormais Grafana voit toute la flotte. Cibles corrigées (bac à sable) : Loki→obs-01,
|
||
relais→edge-mta-01 (pas infra-mail-01, qui est le store Dovecot sans SMTP :25).
|
||
- **Thème de connexion Keycloak à l'identité Alliance Boréale.** Thème de login `alliance-boreale`
|
||
(`roles/serveur_keycloak/files/themes/`, `parent=keycloak` + overlay CSS) reprenant l'identité du
|
||
site de l'Alliance (extraite de `site-alliance-boreale`) : **ciel nocturne aurore** (`#05060f`/
|
||
`#0a0d24` + dégradés), **carte glassmorphism**, **logo étoile aurore** (le `favicon.svg` du site),
|
||
**bouton dégradé aurore** (teal→cyan, pilule), liens cyan, **police système** (souveraineté, zéro
|
||
dépendance externe). Déployé dans `{{ keycloak_home }}/themes/`, appliqué au realm via
|
||
`kcadm ... -s loginTheme` (var `serveur_keycloak_login_theme`, idempotent), Keycloak rechargé
|
||
(`flush_handlers` avant la config realm). **Prouvé** : la page de login charge `alliance.css`
|
||
(HTTP 200) + le `logo.svg` (200), `loginTheme=alliance-boreale` actif sur `chezlepro`.
|
||
**Constellation animée en fond** (`scripts=js/constellation.js`) : le JS crée son propre ciel
|
||
(canvas + aurore, le template n'en ayant pas) — étoiles scintillantes qui dérivent, liens de
|
||
constellation cyan, blob d'aurore ondulant ; respecte `prefers-reduced-motion`. Prouvé :
|
||
`constellation.js` référencé + servi (200).
|
||
**Console de compte thémée aussi** (thème `account`, `parent=keycloak.v3`) : overlay CSS
|
||
surchargeant les variables PatternFly 5 (fond aurore, cartes en verre, accent aurore) + la même
|
||
constellation animée. Var `serveur_keycloak_account_theme` via `kcadm -s accountTheme`. Prouvé :
|
||
console charge (HTTP 200, `keycloak.v3` intact), `account.css` servi (200).
|
||
- **Soumission courriel `:587` interne (authentifiée) — la boucle souveraine est bouclée.**
|
||
Postfix (`edge-mta`) sert la **soumission `:587`** (bloc `master.cf` : STARTTLS requis, `SMTP AUTH`,
|
||
seuls les authentifiés relaient) ; l'auth SASL est **déléguée à Dovecot** (`infra-mail`, passdb LDAP
|
||
prouvé) via un **auth-listener réseau** (`service auth { inet_listener sasl }`, port 12345). Aucune
|
||
sortie internet : interne→interne uniquement (l'externe = déliverabilité, Étape B). **Prouvé** (swaks) :
|
||
`testmail` s'authentifie (`235 Authentication successful`), Postfix accepte (`250 queued`), et le
|
||
courriel est **livré dans la boîte** (LMTP→Dovecot). Le courriel souverain fait maintenant **recevoir
|
||
ET envoyer**. Vars : `serveur_dovecot_sasl_reseau`, `serveur_postfix_submission_actif`. Pièges :
|
||
Dovecot 2.4 exige un **nom** de section `inet_listener` ; ajouter un service `master.cf` (nouveau
|
||
listener) → handler **restart** (pas reload) ; et une config cassée peut bloquer un redéploiement si
|
||
la synchro cert/restart précède le template (corriger la config à la main pour débloquer).
|
||
- **Sauvegardes applicatives (logiques) — `serveur_backup` + `client_backup` (restic), Tier 0 prouvé.**
|
||
Choix : sauvegarder la **donnée d'état** (non régénérable) plutôt que les VM (reconstructibles par
|
||
le code + le template). Outil **restic** (chiffrement côté client, déduplication, rétention).
|
||
`serveur_backup` (nœud `backup-01`) = cible SFTP/SSH (utilisateur `restic`, clé autorisée, dépôts
|
||
sous `/srv/restic/<nœud>`). `client_backup` (intégration par nœud) = restic + **jobs déclaratifs**
|
||
(`client_backup_jobs` : `{nom, commande?, chemins}`), clé SSH + mot de passe restic en voûte,
|
||
script + **timer systemd** (quotidien) + rétention `forget --prune`. **Prouvé de bout en bout**
|
||
sur le **Tier 0** (`infra-pki-01` → `/etc/step-ca`, l'ancre de confiance) : sauvegarde **hors-nœud**
|
||
vers `backup-01`, puis **restauration byte-identique** des clés CA (`root_ca_key`,
|
||
`intermediate_ca_key`, `ca.json`). Piège corrigé : le plancher `/etc/hosts` d'un nœud existant
|
||
ignore un nœud nouvellement ajouté → rafraîchir le socle.
|
||
- **Sauvegardes Tier 1 généralisées — 5 nœuds, restauration prouvée.** `client_backup` étendu (jobs
|
||
déclaratifs en host_vars) à : `data-sql-01` (`pg_dumpall` — keycloak/forgejo/icingadb),
|
||
`id-ldap-01` (`slapcat` LDIF — les identités), `infra-mail-01` (`/var/vmail` — les boîtes),
|
||
`forge-01` (`/var/lib/forgejo` + `/etc/forgejo` — dépôts Git ; la BD est déjà couverte par PG).
|
||
**Prouvé par restauration** : dump PostgreSQL restauré contient bien les 3 bases (`CREATE DATABASE
|
||
forgejo/icingadb/keycloak`) ; LDIF restauré contient `testmail`. Les 5 dépôts restic (pki, sql,
|
||
ldap, mail, forge) sont hors-nœud sur `backup-01`, chiffrés. Ajouter un service à sauvegarder =
|
||
déclarer un job. Reste : cible **offsite** (3-2-1, Étape B — le dépôt n'est qu'une URL swappable).
|
||
- **Consolidation — binding `annuaire` (`resoudre_annuaire`) + retrait de la cruft.**
|
||
*Cruft* : supprimés les 8 dossiers-catégories inertes (`roles/{applications,backup,database,
|
||
identity,monitoring,proxmox,storage,web}/`, README seuls) et les 5 playbooks-échafaudages `debug`
|
||
sans rôle (nextcloud, collabora, client_supervision, web_frontal, web_dorsal) ; l'intention reste
|
||
documentée dans `docs/catalogue-services.md`. *Binding annuaire* : nouveau rôle utilitaire partagé
|
||
`resoudre_annuaire` (comme `resoudre_base`) qui **dérive** la connexion OpenLDAP du `domaine_interne`
|
||
+ un hôte d'annuaire surchargeable — LE seul endroit où le nom d'hôte de l'annuaire est fixé, au lieu
|
||
d'être répété. Facts `resoudre_annuaire_{uri,port,base_dn,users_dn,bind_dn,bind_password}` (secret
|
||
déréférencé, `no_log`). **Migrés + prouvés (config neutre, `changed=0`)** : `serveur_keycloak`
|
||
(fédération LDAP — testmail token HTTP 200), `serveur_dovecot` + `serveur_postfix` (flux courriel
|
||
Postfix→LDAP→LMTP→Dovecot **livré de bout en bout**). Piège appris : les *defaults* d'un rôle inclus
|
||
ne persistent pas hors de son exécution — publier via `set_fact`.
|
||
- **Binding annuaire complété — `icingaweb2` + `client_ldap` migrés vers `resoudre_annuaire`.** Fin des
|
||
2 loose ends : `serveur_icingaweb2` (connexion LDAP dormante en mode SSO) résout via
|
||
`resoudre_annuaire` (redéploiement `changed=0`, SSO intact) ; `client_ldap` (SSSD, dormant) ne pointe
|
||
plus sur un `idm-01` périmé. **Plus AUCUN rôle ne code en dur l'hôte d'annuaire** — un seul point de
|
||
vérité (`resoudre_annuaire`).
|
||
- **Rôle `serveur_oauth2_proxy` — passerelle SSO OIDC générique (Keycloak) + Icinga Web 2 au SSO.**
|
||
oauth2-proxy (v7.15.3, binaire GitHub) place Keycloak **devant** n'importe quelle app sans OIDC
|
||
natif : elle reçoit l'utilisateur authentifié via en-tête, en auth `external`. Rôle paramétrable
|
||
(client, secret voûte, redirect, upstream, cookie voûte) — **réutilisable** pour toute app
|
||
OIDC-less. Éprouvé sur `sup-01` **devant Icinga Web 2** : client Keycloak `icingaweb2`,
|
||
oauth2-proxy `:4180` (exposé par l'edge) → upstream nginx local `:8080` → icingaweb2
|
||
`backend = external` (REMOTE_USER depuis `X-Forwarded-Preferred-Username`). **Prouvé** (flux
|
||
authorization code headless) : `testmail` → oauth2-proxy → Keycloak → **icingaweb2 `/dashboard`,
|
||
connecté** (« Se connecter avec Chezlepro », comme Grafana/Forgejo). Ferme le gap LDAP-direct
|
||
d'icingaweb2. **Réglages appris** : `insecure_oidc_allow_unverified_email` (les users LDAP n'ont
|
||
pas `email_verified` ; IdP interne de confiance) ; en reverse-proxy oauth2-proxy passe
|
||
`X-Forwarded-*` (pas `X-Auth-Request-*`) ; **handler nginx en `restart` (pas `reload`)** car un
|
||
changement d'adresse d'écoute n'est pas pris par un reload gracieux.
|
||
- **Rôle `serveur_icingaweb2` — Icinga Web 2 (UI native) + module IcingaDB : éprouvé.** App PHP
|
||
(php8.4-fpm) servie par un **nginx local**, exposée par l'edge (`icinga.lab.chezlepro.internal`,
|
||
auto-dérivé : vhost + cert SAN + A PowerDNS + alias plancher). Config **par fichiers `.ini`**
|
||
(config/resources/authentication/roles + module `icingadb`), pas d'assistant de setup. Base
|
||
IcingaDB via `resoudre_base` (registre). **Auth LDAP direct** vers OpenLDAP (LDAPS, `client_pki`
|
||
sur `sup-01`) — icingaweb2 n'a pas d'OIDC natif ; SSO-par-proxy = raffinement futur. **Prouvé** :
|
||
`testmail` (LDAP) se connecte (`/dashboard`), et le **module IcingaDB affiche la supervision**
|
||
(hôte `icinga`). Déploiement `failed=0` (le rôle est bon ; les frictions étaient dans le
|
||
simulateur de login curl : contrôle de cookie `_checkCookie`, champs `uid`/`submit_login`,
|
||
valeur CSRF avant `name`).
|
||
- **Module BPM (Business Process) éprouvé + codifié — pile Icinga complète.** `serveur_icingaweb2`
|
||
installe + active `icingaweb2-module-businessprocess` (`serveur_icingaweb2_modules`), crée le
|
||
répertoire des processus (éditable via l'UI, groupe `icingaweb2`, setgid) et **sème des processus
|
||
métier en IaC** (`serveur_icingaweb2_bpm_processes`, nom → contenu `.conf`). Format des feuilles
|
||
`host;service` (éprouvé via les fixtures du module). **Prouvé** : un processus « Supervision
|
||
Chezlepro » (agrège load/procs/swap/ping4/ssh du host `icinga` en logique ET) **rend un état**
|
||
dans l'UI (`testmail` connecté), **avec le backend IcingaDB** (pas d'IDO). Rôle re-prouvé (reset
|
||
→ recrée le processus, idempotent). BPM n'est **pas remplaçable par Grafana** (roll-up d'impact
|
||
métier). Pile Icinga = **moteur + Web 2 + BPM**, complète.
|
||
- **Cœur Icinga éprouvé (supervision active).** `serveur_icinga` (cœur : `icinga2` + `icingadb` +
|
||
`icingadb-redis`) déployé sur `sup-01` (🔧→⭐), base `icingadb` PostgreSQL via le registre.
|
||
**Prouvé** : 3 services actifs, et le moteur **supervise** — IcingaDB peuplée (1 hôte, 12
|
||
services, résultats de checks persistés en base). Zéro bug de déploiement (rôle bien bâti).
|
||
**Icinga Web 2 + module BPM restent différés** (phases dédiées : UI native + vues d'impact
|
||
métier ; le BPM n'est pas remplaçable par Grafana). Confirme aussi le **DRY `resoudre_base`**
|
||
sur icinga (les 3 rôles consommateurs validés).
|
||
- **Forgejo branché au SSO OIDC (« Se connecter avec Chezlepro ») — 2e app SSO, prouvée.**
|
||
`serveur_forgejo` (10.0.0) éprouvé sur `forge-01` (🔧→⭐), adossé à PostgreSQL (base `forgejo`
|
||
auto-provisionnée depuis le registre), exposé par l'edge (vhost + cert SAN + A PowerDNS + alias
|
||
plancher, tout auto-dérivé de `expose`). **Source OAuth2** vers Keycloak
|
||
(`forgejo admin auth add-oauth`, idempotent, realm `chezlepro`), client OIDC `forgejo`
|
||
enregistré via `serveur_keycloak_clients`. Auto-enregistrement OIDC (`[oauth2_client]
|
||
ENABLE_AUTO_REGISTRATION` + `ALLOW_ONLY_EXTERNAL_REGISTRATION` : identités depuis l'annuaire
|
||
seulement). **Prouvé** (flux authorization code headless) : `testmail` (LDAP) se connecte,
|
||
**compte auto-créé** (`testmail@lab.chezlepro.internal`), atterrit sur le tableau de bord.
|
||
**5 bugs de 1er déploiement corrigés** : dépendance périmée `serveur_sendmail`→`serveur_postfix`
|
||
(le vrai MTA) ; `app.ini` doit appartenir au user `git` (Forgejo persiste des secrets générés) ;
|
||
ordre admin/migrations (`flush_handlers` + `wait_for` avant `admin user create`) ; `HTTP_ADDR`
|
||
`127.0.0.1`→`0.0.0.0` (l'edge nginx est sur un autre hôte, 502 sinon) ; auto-enregistrement OIDC.
|
||
- **DRY : rôle utilitaire partagé `resoudre_base` (résolution BD depuis le registre).** Le bloc
|
||
copié-collé dans `serveur_keycloak`, `serveur_forgejo` et `serveur_icinga` (charger le registre,
|
||
filtrer par consommateur, déréférencer le secret via `lookup('vars', ...)`, résoudre hôte/port)
|
||
est extrait dans `roles/resoudre_base` (facts `resoudre_base_entree/db_password/db_host/db_port`,
|
||
`no_log`). Les 3 rôles l'incluent (`include_role`) et adoptent les facts. Le secret **ne quitte
|
||
toujours pas le rôle** (déréférencé au déploiement). **Fait « sur la preuve »** : re-déploiement
|
||
keycloak + forgejo `failed=0`, idempotent, `testmail` token Keycloak HTTP 200. Ferme le reste
|
||
noté de la Phase 2 des bindings (cf. `docs/bindings-conception.md`).
|
||
- **PowerDNS — A d'exposition auto-dérivés (le DNS de la Phase 3 des bindings).** `serveur_powerdns`
|
||
génère désormais, dans la zone interne, un enregistrement A pour chaque FQDN d'exposition (champ
|
||
`expose` des applications) vers l'**edge qui le sert** (`domaines.edge`) — via
|
||
`expositions_des_applications` (même source que les vhosts nginx et les SANs du cert edge).
|
||
Déclarer `expose` produit maintenant **vhost + SAN de cert + enregistrement DNS**, tout dérivé.
|
||
**Prouvé** : `dig @infra-dns-01 grafana.lab.chezlepro.internal` et `keycloak.…` → `192.168.15.21`
|
||
(edge). Option `serveur_powerdns_publier_expositions` (défaut true). **Limite / reste** : PowerDNS
|
||
est **autoritatif, pas récursif** — pour que les nœuds *utilisent* ces A sans casser la résolution
|
||
Internet, il faut un **récursif** (pdns-recursor : forward de la zone interne + récursion du reste)
|
||
ou garder le plancher `/etc/hosts`. Ne PAS repointer naïvement `client_dns` vers l'autoritatif.
|
||
- **`hosts_statiques` — alias d'exposition dans le plancher `/etc/hosts` (résolution client, sûre).**
|
||
Le plancher pose désormais, sur **chaque nœud**, `<IP edge> <FQDN exposé>` pour chaque `expose`
|
||
(dérivé de `domaines.edge`, même source que nginx/PowerDNS). Indépendant du DNS, aucun risque de
|
||
couper la résolution (choix retenu vs pdns-recursor). Chargement du plan **best-effort**
|
||
(`stat` **`delegate_to: localhost`** + `become: false` — les registres vivent sur le nœud de
|
||
contrôle ; ignoré si le plan est absent, ex. préparation du template). **Prouvé** : `/etc/hosts`
|
||
d'obs-01 régénéré avec `keycloak`/`grafana` → edge (ligne manuelle éliminée), `getent` OK, et le
|
||
**flux SSO Grafana fonctionne via la résolution du plancher** (`login: testmail`). Boucle Phase 3
|
||
fermée : déclarer `expose` → **vhost + SAN cert + A PowerDNS + alias plancher**, tout dérivé.
|
||
Bugs corrigés en chemin : `serveur_loki` (groupe `loki` manquant), `stat` sur cible→contrôle,
|
||
`become` inutile sur le contrôle.
|
||
- **Rôle `client_unbound` — résolveur local (DNS dynamique) : éprouvé sur un nœud.** Unbound par
|
||
nœud (`127.0.0.1`) avec **stub-zone** vers l'autoritatif interne (PowerDNS) + **récursion**
|
||
Internet (ou forward via `client_unbound_transitaires`). Alternative *dynamique* au plancher
|
||
`/etc/hosts` statique, sans casser Internet. Bascule de `/etc/resolv.conf` **protégée**
|
||
(`client_unbound_apply` + `client_unbound_confirm`) **et validée AVANT** (Unbound doit résoudre
|
||
interne + Internet, sinon pas de bascule → nœud jamais coupé). **Prouvé sur data-sql-01** (rayon
|
||
d'impact minimal) : `dig @127.0.0.1 keycloak/id-sso-01.lab.chezlepro.internal` → PowerDNS,
|
||
`deb.debian.org` → récursion, `apt` OK. Rôle sûr par défaut (`apply: false` : installe Unbound
|
||
sans toucher au resolver). Rollout flotte = opt-in par nœud. **Note direction** : OPNsense
|
||
embarque Unbound → à terme, l'Unbound *réseau* peut vivre sur l'appliance de bordure (nœud
|
||
public, Étape B) ; le rôle par-nœud reste portable et complémentaire (cache local).
|
||
- **Bindings — Phase 1 : résolveur de liens dans `instancier.py` (relations service→service
|
||
déclaratives).** Une application déclare ses `liens: [{vers, role}]` dans
|
||
`plan/applications.yml` ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml`
|
||
(`setops_liens.accepte`, comme `meta/empreinte.yml`). `instancier` résout la cible (FQDN interne
|
||
**dérivé de la nomenclature** + `domaine_interne`), substitue les gabarits (`{cible.fqdn}`,
|
||
`{cible.hote}`, `{cible.ip}`) et **injecte les variables en host_vars du consommateur**.
|
||
Validation : rôle accepteur, cible existante, genre attendu. **Migration prouvée** : les liens
|
||
mail Postfix→Dovecot (`mailstore`) et Postfix→rspamd (`milter`) passent de group_vars codés en
|
||
dur à des liens déclaratifs — `make instancier` donne **DIFF VIDE** (mêmes variables générées),
|
||
puis les group_vars sont retirés. La topologie mail devient déclarative et portable.
|
||
Cf. `docs/bindings-conception.md`. Suite : bases (Phase 2), exposition/domaines (Phase 3), GUI (Phase 4).
|
||
- **Bindings — Phase 2 (bases) : constat + réconciliation de la note (`docs/bindings-conception.md` §5/§9).**
|
||
Inspection du code réel : le binding app→base **existe déjà** — **côté base** (`consommateur`/`portee`
|
||
dans `bases-donnees.yml`), résolu **dans le rôle** au déploiement (`include_vars` + filtre +
|
||
`lookup('vars', secret)`), sur 4 rôles (postgresql, forgejo, keycloak, icinga). **Délibérément
|
||
conservé** (le secret ne quitte jamais le rôle) — ne PAS dupliquer en app-side/instancier. Deux
|
||
directions assumées : app→app côté app (instancier), app→base côté base (registre). Reste (reporté
|
||
à l'épreuve de Keycloak) : factoriser le bloc de résolution copié-collé en include partagé (DRY).
|
||
- **`docs/carte-set-ops.md` — carte d'orientation (index + mécanismes transverses).** Après audit
|
||
du dépôt : point d'entrée « à lire d'abord » (index du corpus, ~22 docs), et **catalogue des
|
||
mécanismes** dispersés dans le code (les 2 directions de binding, pont de cert, résolution BD
|
||
par registre, socle-first, sûreté check-mode, voûte, dimensionnement) avec **où ils vivent**.
|
||
But : ne plus re-découvrir l'existant. Constat : la **cruft était déjà inventoriée** dans
|
||
`catalogue-services.md` (rôles-catégories inertes, échafaudages) — non dupliquée, référencée.
|
||
`catalogue-services.md` « État d'implémentation » **rafraîchi** (rôles éprouvés sur VM réelles :
|
||
socle, PKI, LDAP, DNS, nginx, pile courriel). Pointeur ajouté depuis `architecture-set-ops.md`.
|
||
- **`serveur_postgresql` et `serveur_keycloak` éprouvés sur VM réelles (🔧→⭐).** PostgreSQL
|
||
déployé (data-sql-01), écoute réseau + pg_hba VLAN, et **provisionne la base `keycloak` depuis
|
||
le registre** (`bases-donnees.yml`) — **binding app→base prouvé en réel** (base + rôle créés,
|
||
mot de passe = `vault_bd_keycloak`). Keycloak 26.0.7 déployé (id-sso-01), **mode prod**,
|
||
connecté à PostgreSQL (**87 tables** du realm master écrites), **token admin obtenu** (auth
|
||
adossée à la BD). Lacunes connues (documentées `catalogue-services.md`) : fédération LDAP et
|
||
edge nginx pas encore câblés. Reste : DRY du bloc de résolution BD (keycloak/forgejo/icinga).
|
||
- **`serveur_keycloak` — fédération LDAP (modèle d'identité A) : automatisée et prouvée.** Le rôle
|
||
configure, via `kcadm` (idempotent), un realm applicatif (`serveur_keycloak_realm`, déf.
|
||
`chezlepro`) et un **provider de stockage LDAP READ_ONLY** vers OpenLDAP (LDAPS, `uid`/`entryUUID`,
|
||
`inetOrgPerson`). TLS LDAPS validé via le **truststore système** (`truststore-paths` →
|
||
`/etc/ssl/certs/ca-certificates.crt`, racine step_ca posée par **client_pki**, désormais requis
|
||
sur le nœud). Secrets par `environment` + `no_log`. **Éprouvé avant codification** puis prouvé
|
||
par le rôle : un utilisateur LDAP (`testmail`) obtient un token via le realm (HTTP 200), et le
|
||
redéploiement est **idempotent** (`changed=0`). Nouveau : `tasks/federation-ldap.yml`. Reste :
|
||
edge nginx (accès HTTPS par nom), mappers d'attributs/groupes fins.
|
||
- **Edge nginx + exposition (Phase 3 des bindings) — prouvés avec Keycloak.** Sans changement de
|
||
code : la machinerie `serveur_nginx_publier_expositions` **existait déjà** (lit `expose` des
|
||
applications + `edge` de `domaines.yml`, dérive `amont = http://<IP hôte>:<port>`, génère le
|
||
vhost avec `X-Forwarded-*`). Le bac à sable déclare `keycloak.expose:
|
||
[keycloak.lab.chezlepro.internal]` + le domaine interne `lab.chezlepro.internal` (edge
|
||
`serveur_nginx`). **Prouvé** : le vhost s'auto-génère (`keycloak.lab.chezlepro.internal →
|
||
http://192.168.15.81:8080`), et la découverte OIDC via l'edge renvoie
|
||
`"issuer":"https://keycloak.lab.chezlepro.internal/..."` (les `X-Forwarded` passent, Keycloak
|
||
se sait derrière HTTPS). **Limite connue** : le cert TLS de l'edge est encore le snakeoil
|
||
auto-signé (avertissement navigateur). **Raffinement recommandé** (réutilise l'existant, pas de
|
||
nouveau mécanisme) : ajouter les FQDN d'exposition aux `client_pki_sans` de l'edge (client_pki
|
||
demande + renouvelle déjà le cert d'hôte), puis pointer `serveur_nginx_certificat` sur le cert
|
||
client_pki (`/etc/step/certs/<edge>.crt`).
|
||
- **Cert de l'edge : snakeoil → step_ca (HTTPS valide).** Appliqué le raffinement ci-dessus :
|
||
`client_pki` ajouté à l'edge, ses `client_pki_sans` incluent le FQDN d'exposition
|
||
(`keycloak.lab.chezlepro.internal`), et `serveur_nginx_certificat`/`_cle` pointent sur le cert
|
||
client_pki. **Prouvé** : HTTPS `HTTP 200` avec `ssl_verify_result=0` (chaîne validée contre la
|
||
racine step_ca, nom correct), émetteur `Set-OPS Internal CA`. Sans nouveau code (client_pki +
|
||
group_var). **Gaps notés** : (1) recharger nginx au **renouvellement** du cert (le cert-renewer
|
||
renouvelle en place, nginx ne recharge pas seul — hook à ajouter) ; (2) **auto-dériver** les SANs
|
||
d'exposition de l'edge depuis le plan (au lieu de les lister dans le group_var).
|
||
- **Grafana branché au SSO OIDC (« Se connecter avec Chezlepro ») — prouvé de bout en bout.**
|
||
`serveur_grafana` : config OIDC via `GF_AUTH_GENERIC_OAUTH_*` (client confidentiel `grafana`,
|
||
realm `chezlepro`, secret `vault_grafana_oidc`). `serveur_loki` + `serveur_prometheus` +
|
||
`serveur_grafana` déployés sur `obs-01` (🔧→⭐). **Bug de rôle corrigé** : `serveur_loki` créait
|
||
le répertoire en `group: loki` alors que le paquet crée l'utilisateur en `nogroup` sans groupe
|
||
`loki` → ajout de la création du groupe. **Prouvé (flux authorization code headless, via l'edge
|
||
HTTPS)** : `testmail` (user LDAP) se connecte à Grafana par le SSO — `/api/user` renvoie
|
||
`login: testmail`, email et nom **fédérés depuis LDAP**. Chaîne complète LDAP → Keycloak →
|
||
Grafana. **Gaps notés (pour rendre 100 % déclaratif)** : (1) l'**enregistrement du client OIDC**
|
||
dans Keycloak a été fait via `kcadm` **à la main** (à codifier — rôle grafana ou liste de clients
|
||
côté keycloak) ; (2) la **résolution** `keycloak.…internal → edge` sur obs-01 est un `/etc/hosts`
|
||
manuel (**PowerDNS devrait porter les A d'exposition** — chaînon récurrent) ; (3) mapping de
|
||
rôles Grafana (tous Viewer par défaut).
|
||
- **`serveur_keycloak` — enregistrement des clients OIDC codifié (gap précédent fermé).** Le rôle
|
||
gère une **liste déclarative** `serveur_keycloak_clients` (`clientId`, `redirect_uris`,
|
||
`web_origins`, `secret`) et enregistre chaque client confidentiel via **kcadm idempotent**
|
||
(`tasks/clients-oidc.yml`, create-si-absent, `no_log`). Décision : **côté Keycloak** (les creds
|
||
admin restent dans le seul rôle Keycloak, pas répandus dans chaque rôle app) ; le `secret`
|
||
référence la même variable de voûte que l'app. **Prouvé** : client `grafana` supprimé → rôle →
|
||
recréé → `testmail` se connecte à Grafana (`login: testmail`) ; redéploiement **idempotent**
|
||
(`changed=0`). Le déploiement de Grafana au SSO est désormais **autonome**.
|
||
|
||
## 2026-07-02
|
||
|
||
### Décidé
|
||
- **Bindings — conception des relations app/base/serveur/domaine (`docs/bindings-conception.md`).**
|
||
Les relations service→service sont aujourd'hui codées en dur, éparpillées dans des group_vars
|
||
(ex. Postfix→Dovecot/rspamd/LDAP), ce qui casse la portabilité multi-tenant. Direction retenue :
|
||
**liens déclarés côté application** (`liens: [{vers, role}]`), résolus par `instancier.py` en
|
||
variables Ansible ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml` (comme
|
||
`meta/empreinte.yml`) ; FQDN cible **dérivé de la nomenclature** (jamais codé en dur). Domaines
|
||
publics traités comme lien `exposition` (écrit sur l'edge). Réconcilie l'existant (bases
|
||
`consommateur`, `domaines.edge`). Preuve de migration ciblée : les 3 liens mail. Implémentation
|
||
à suivre (phasée).
|
||
- **Licence : passage de CC BY-NC-SA 4.0 à AGPLv3.** Les licences Creative Commons ne sont pas
|
||
faites pour du logiciel (position de CC elle-même) et la clause **NonCommercial contredisait
|
||
le principe fondateur « tout est libre »** — en plus de bloquer les artisans/coopératives
|
||
visés. `LICENSE` remplacé par le **texte officiel intégral de l'AGPLv3** (verbatim, non
|
||
modifié). Attribution + modèle **libre + services/certification** documentés dans le `README`
|
||
(méthode d'attribution recommandée, sans toucher au texte de la licence). L'AGPLv3 protège la
|
||
souveraineté (anti-captation propriétaire en SaaS) **sans interdire l'usage commercial**.
|
||
- **Architecture d'identité/SSO (`docs/identite-sso.md`).** Modèle A : **OpenLDAP source de
|
||
vérité**, **Keycloak fédéré** (SSO web OIDC, MFA, self-service), **mail en bind LDAP
|
||
direct**. Une identité, un mot de passe, deux chemins d'auth (web→Keycloak, mail→LDAP),
|
||
tout adossé au même OpenLDAP. Sert la portabilité multi-tenant (chaque tenant = son LDAP +
|
||
son Keycloak fédéré). Pas Keycloak-source (casse-tête mail + moins portable).
|
||
- **Service courriel : pivot de Stalwart vers Postfix + Dovecot + rspamd.** La Phase 1
|
||
Stalwart (`serveur_stalwart`) avait été prototypée et déployée (v0.16.11, install +
|
||
démarrage en mode récupération). Le prototypage a révélé un projet **trop jeune/volatil
|
||
pour un pilier mail critique** : config cassée entre 0.15 et 0.16, outil IaC
|
||
`stalwart config apply` **annoncé mais non livré** dans le binaire, API REST supprimée
|
||
(JMAP), gros backlog. Pivot vers la stack **mature Postfix/Dovecot/rspamd**, en prime
|
||
**100 % configurable par fichiers** (alignée au modèle déclaratif Set-OPS). Le rôle
|
||
`serveur_stalwart` est **retiré** (git en garde la trace) ; `docs/courriel-conception.md`
|
||
mis à jour. Réévaluer Stalwart ~2028.
|
||
|
||
### Ajouté
|
||
- **`docs/pouvoirs-set-ops.md` — bilan des capacités du moteur.** Inventaire structuré
|
||
(moteur/plan, plan de contrôle GUI, socle durci, piliers d'infrastructure, services outillés,
|
||
patrons d'ingénierie), distinguant honnêtement « prouvé sur cluster réel » de « outillé ».
|
||
- **Rôle `serveur_rspamd` (rspamd 3.x) — antispam + DKIM, en milter sur Postfix.** Installé
|
||
sur le nœud edge-mta (avec Postfix), backend **Redis** local, worker proxy en **mode milter
|
||
auto-scan** (`:11332`), **signature DKIM** sortante (clé générée par le rôle de façon
|
||
idempotente ; enregistrement DNS public affiché pour l'Étape B). Config par surcharges
|
||
`/etc/rspamd/local.d/`. Postfix branché via `smtpd_milters` (option `serveur_postfix_rspamd_milter`,
|
||
`milter_default_action = accept` → tolérant si rspamd indisponible). **Prouvé** : un courriel
|
||
traversant le milter ressort **scanné** (`rspamc stat` : 1) et **signé DKIM** (`DKIM-Signature:
|
||
d=…`), puis livré et lu en IMAP. **Étape A (courriel interne) complète** : dovecot + postfix + rspamd.
|
||
- **Flux courriel interne PROUVÉ de bout en bout (Étape A).** Envoi → Postfix (`edge-mta`,
|
||
validation LDAP) → LMTP réseau → Dovecot (mail-store) → boîte Maildir → **lu en IMAP**
|
||
(auth LDAP, TLS step_ca) : `status=sent`, message lu (sujet + corps). Réglages Dovecot 2.4
|
||
qui débloquent la remise LMTP : `userdb static { static_allow_all_users = yes }` (sinon
|
||
NOTFOUND pour l'expéditeur/raw-mail-user externe), `mail_inbox_path =` vidé (le défaut mbox
|
||
`/var/mail` root refusait l'autocréation de l'INBOX), Maildir explicite (`mail_home` +
|
||
`mail_path = %{home}/Maildir`), chemin par nom d'utilisateur (home identique côté LMTP
|
||
local-part et IMAP adresse complète ; mono-domaine, multi-domaine = raffinement Étape B).
|
||
- **Rôle `serveur_postfix` (Postfix 3.x) — MTA du nœud edge-mta.** Réception `:25`, cartes
|
||
**LDAP** (validation des boîtes via l'attribut `mail`), remise **LMTP réseau** vers le nœud
|
||
mail-store Dovecot (`virtual_transport = lmtp:inet:[…]:24`), TLS via **step_ca** (pont de
|
||
cert), aucune boîte locale. Config `main.cf` + carte `ldap-mailboxes.cf`, validée par
|
||
`postfix check`. Secret de bind : `vault_openldap_admin`. Nécessite `serveur_postfix_mailstore_hote`
|
||
(FQDN du mail-store). Validé statiquement ; déploiement réel à suivre.
|
||
- **Rôle `serveur_dovecot` (Dovecot 2.4) — déployé et prouvé.** IMAP `:993`/`:143` +
|
||
LMTP, **auth/annuaire LDAP** (vers `serveur_openldap`, filtre `mail`), stockage Maildir
|
||
(user système `vmail`), **TLS via step_ca** (pont de cert + resync au renouvellement),
|
||
neutralisation de l'auth système par défaut. Config en drop-in **syntaxe Dovecot 2.4**
|
||
(`mail_driver`, `ssl_server_cert_file`, `passdb ldap`/`userdb static`, `%{user}`),
|
||
**validée par `doveconf`** au déploiement. Sockets d'intégration Postfix **conditionnels**
|
||
(rendus si l'utilisateur `postfix` est co-localisé). Prouvé : `doveadm auth test` — bon
|
||
mot de passe accepté, mauvais refusé, sur cert step_ca. Secret de bind : `vault_openldap_admin`.
|
||
- **`serveur_openldap` durci pour la prod : TLS via step_ca + organisation en intrant.**
|
||
- **TLS (LDAPS + STARTTLS)** : le certificat d'hôte step_ca (déposé par `client_pki`,
|
||
`root:root 600`) est synchronisé vers un emplacement lisible par `openldap` (`/etc/ldap/tls`)
|
||
par un script + une unité `path` systemd qui **re-synchronise et recharge slapd à chaque
|
||
renouvellement** ; `olcTLS*` configuré dans `cn=config`, `SLAPD_SERVICES` expose `ldaps://`.
|
||
Dégrade proprement (slapd en clair local) si `client_pki` n'a pas encore posé le cert.
|
||
- **Organisation** : nouvel intrant `chezlepro_organisation` (remplace le « Exemple Inc » codé).
|
||
- *Écrit en code de prod, éprouvé statiquement ; validation par déploiement réel à suivre.*
|
||
- **`docs/courriel-conception.md`** — cadrage du futur service de courriel souverain :
|
||
full self-host, suite **Stalwart** (adoptée, enveloppée par un rôle mince), topologie
|
||
MX primaire + MX secours, intégration identité (LDAP) / DNS / PKI (Let's Encrypt public
|
||
vs step_ca interne), enregistrements DNS publics, décisions ouvertes (IP/PTR, secours,
|
||
stockage) et phasage. **Conception seulement — aucun rôle livré.**
|
||
|
||
## 2026-07-01
|
||
|
||
### Ajouté
|
||
- **État RÉEL vs plan dans le GUI (sonde de vie + auto-actif).** Le badge `planifié`/
|
||
`actif` décrit l'*intention* du plan, pas l'existence de la VM — d'où la confusion
|
||
« serveur planifié mais vivant ». Deux ajouts :
|
||
- **Sonde de vie** : le GUI teste la joignabilité SSH de chaque hôte en arrière-plan
|
||
(`/api/sondes`, en parallèle) et affiche un état réel — **● vivante** / **● injoignable**
|
||
— sur les tuiles et dans le détail (« Plan : … · Réel : … »), distinct du plan.
|
||
- **Auto-actif** : un hôte qu'on **matérialise** (clone `creer` réussi) ou qu'on
|
||
**déploie** passe automatiquement `actif` dans le plan (matérialisé = actif), puis
|
||
l'inventaire est **régénéré** pour que le changement se voie partout (en-tête inclus).
|
||
- **Compteur « vivantes »** dans l'en-tête (depuis la sonde), à côté de actifs/planifiés.
|
||
|
||
### Corrigé
|
||
- **nginx ne validait pas sur Debian 13 (`server_tokens` en double).** Debian 13 livre
|
||
`server_tokens off;` **actif** dans `/etc/nginx/nginx.conf` (avant : commenté). Le
|
||
drop-in `conf.d/99-setops.conf` du rôle le redéclarait → `nginx -t` échouait
|
||
(« directive is duplicate ») et le déploiement plantait au handler de validation. Le
|
||
rôle `serveur_nginx` neutralise désormais la ligne distro (le drop-in reste l'unique
|
||
source). Trouvé en déployant nginx pour de vrai sur un hôte edge.
|
||
- **Une voûte chiffrée cassait `instancier` / « Appliquer le plan ».** `ansible-inventory
|
||
--list` (utilisé pour la comparaison sémantique du plan) tente de déchiffrer
|
||
`group_vars/all/vault.yml` et échoue sans mot de passe (`exit 4`) — alors que
|
||
l'opération est structurelle, sans secret. `instancier` utilise désormais
|
||
automatiquement le fichier conventionnel `~/.config/setops-vault-pass` (si
|
||
`ANSIBLE_VAULT_PASSWORD_FILE` n'est pas déjà défini).
|
||
- **Secrets des rôles non câblés à la voûte (échafaudage manquant).** Les rôles à
|
||
secrets déclaraient `serveur_X_password: ""` avec, en commentaire seulement, la
|
||
variable de voûte attendue (`{{ vault_X }}`) — sans mapping réel. Résultat : remplir
|
||
la voûte selon `vault.exemple.yml` ne suffisait pas, le secret restait vide et
|
||
l'assertion « secrets requis » échouait. Les 12 secrets des 8 rôles (`serveur_step_ca`,
|
||
`serveur_forgejo`, `serveur_grafana`, `serveur_keycloak`, `serveur_openldap`,
|
||
`serveur_redis`, `client_ldap`, `client_pki`) pointent désormais vers leur variable
|
||
de voûte : `serveur_X_password: "{{ vault_X | default('') }}"`. Chaque instance n'a
|
||
plus qu'à remplir ses `vault_*` dans sa voûte chiffrée ; aucun mapping par instance.
|
||
- **« Vérifier » (dry-run `--check`) échouait faussement sur un hôte frais.** Les tâches
|
||
« démarrer service » et les handlers « redémarrer / recharger / valider » des rôles
|
||
applicatifs touchent un paquet que `--check` n'installe pas réellement → le service
|
||
(ou le fichier de zone/conf) n'existe pas encore → faux `fatal`, qui **bloquait le
|
||
déploiement** (le dry-run doit réussir pour débloquer « Déployer »). Ajout de
|
||
`when: not ansible_check_mode` sur ces tâches et handlers des 13 rôles `serveur_*`
|
||
(29 gardes). En dry-run elles sont sautées ; en vrai déploiement, inchangées.
|
||
- **Le bouton ⚙ « Appliquer le plan » du GUI refusait en silence** dès que le plan
|
||
divergeait de l'inventaire (il appelait `instancier appliquer` **sans** `--force`).
|
||
Résultat : après une édition (disque, état, auto-actif), l'inventaire n'était jamais
|
||
régénéré et les compteurs restaient figés. Le clic « Appliquer » **est** l'intention
|
||
explicite → le GUI force désormais (git reste le filet).
|
||
|
||
- **Bouton « Pousser » dans le GUI — le flux devient 100 % cliquable.** Chaque objet
|
||
du détail se matérialise sur son hôte, en streaming console (avec mot de passe vault
|
||
+ confirmation renforcée en prod) :
|
||
- **Serveur** → `🖥 Pousser` clone la VM depuis le golden template (`make creer-vm`),
|
||
disponible même sur un hôte planifié — comble le trou : le GUI ne clonait pas.
|
||
- **Application** → `Pousser` déploie l'hôte porteur (`make deployer HOTE=<hôte>`).
|
||
- **Base** → `Pousser` déploie l'hôte du serveur de BD (crée la base).
|
||
Nouveaux modes `creer` / `pousser` dans `executer_flux` + routes `/api/creer` et
|
||
`/api/pousser`. Flux complet depuis l'interface : éditer → ⚙ Appliquer → 🖥 Pousser
|
||
→ Vérifier → Déployer.
|
||
- **Premier déploiement RÉEL validé de bout en bout** (cluster asgard) : flux canonique
|
||
plan-piloté clonant + durcissant + faisant tourner un PowerDNS qui résout. Voir les 3
|
||
correctifs ci-dessous.
|
||
|
||
## 2026-06-30
|
||
|
||
### Ajouté
|
||
- **Plancher de résolution `/etc/hosts` (indépendant du DNS).** Nouveau rôle de socle
|
||
`hosts_statiques` (dans `serveur_debian`) : génère `/etc/hosts` sur **chaque** VM
|
||
depuis l'inventaire (nom + FQDN interne → IP réelle). Tout l'écosystème se résout
|
||
par nom **même serveur DNS éteint**, et le bootstrap ne dépend plus du DNS. PowerDNS
|
||
devient une **commodité** (zone/externe/dynamique) ; `client_dns` est rendu tolérant
|
||
(inerte si aucun DNS interne) et sa dépendance à `serveur_powerdns` passe **molle**.
|
||
- **Adressage fédéré : index d'instance.** Le VMID n'est plus codé `9CSNN` en dur :
|
||
il prend le préfixe d'un `index` déclaré en tête de `plan/nomenclature.yml`
|
||
(`{index}{catégorie}{service}{séq}`). Convention : `supernet = 10.(10+index).0.0/16`,
|
||
`VMID = index·CSNN`. Permet à N écosystèmes de **coexister/s'interconnecter** sans
|
||
collision (Chezlepro=1 → `10.11`/`1xxxx`, Technolibre=2 → `10.12`/`2xxxx`). Sans
|
||
index → `9CSNN` (rétro-compatible ; bacs à sable, plages ad-hoc `172.19.x`).
|
||
Code mort retiré (`deriveServeur` JS). Voir `docs/multi-instances.md`.
|
||
- **Doc `docs/multi-instances.md`** : cadrage « un moteur, N écosystèmes » — l'instance
|
||
comme dépôt autonome, la bascule, l'isolation, et le socle multi-tenant.
|
||
- **Bascule d'instance (`make instance-utiliser NOM=…`).** Repointe le symlink
|
||
`instance` vers un autre dépôt d'instance (prod ↔ bac à sable) ;
|
||
`make instance-courante` affiche l'instance montée. Permet d'exploiter plusieurs
|
||
instances (séparation **par instance**) depuis un seul moteur.
|
||
- **Identité des intrants relative à l'inventaire.** Le panneau « Intrants » lit/écrit
|
||
l'identité dans `group_vars/all/10-intrants.yml` de l'inventaire monté (fichier réel
|
||
pour une instance autonome, symlink vers une source partagée sinon — l'écriture suit
|
||
le symlink). Fonctionne donc aussi bien pour une instance « par instance » que pour
|
||
l'ancien partage lab/production.
|
||
- **Inventaire d'instance neutre et configurable (`principal` / `SETOPS_INVENTAIRE`).**
|
||
Le moteur (Makefile + `inventory_gui`, `instancier`, `config_proxmox`, `serveurs`,
|
||
`applications`) ne code plus en dur `inventories/lab` / `inventories/production` :
|
||
il vise **un inventaire par instance**, détecté de façon **rétro-compatible**
|
||
(`principal` > `production` > `lab`) et surchargeable par `SETOPS_INVENTAIRE`. Les
|
||
instances existantes (découpage lab/production) continuent de fonctionner à
|
||
l'identique ; les nouvelles peuvent adopter `inventories/principal/`. Deuxième pierre
|
||
de la séparation **par instance** (la 1re étant le drapeau `setops_production`).
|
||
- **Garde-fou de prudence par instance (`setops_production`).** Le déploiement réel
|
||
est désormais possible sur **toute instance** (un bac à sable déploie sur *son*
|
||
infra lab — il est isolé **et** fonctionnel). Le drapeau `setops_production` dans
|
||
`group_vars/all/` ne **bloque** plus rien : il marque la PRODUCTION pour exiger une
|
||
**confirmation renforcée** au déploiement (bannière/badge rouge « PROD », bouton
|
||
Déployer en rouge, re-saisie du nom d'hôte) ; un bac à sable (`false`) affiche
|
||
« bac à sable » et déploie sans cette étape. Rétro-compatible (à défaut de drapeau,
|
||
ancien repère « inventaire production »). Le GUI affiche toujours quel type
|
||
d'instance est monté.
|
||
|
||
### Modifié
|
||
- **Voûte de secrets unique par environnement.** Fini les voûtes éparpillées : tous
|
||
les secrets de l'instance (token Proxmox + 17 `vault_*` pour PKI, LDAP/SSO, bases,
|
||
forge, observabilité) vivent dans **un seul fichier chiffré**,
|
||
`inventories/<env>/group_vars/all/vault.yml`. Gabarit committé
|
||
`exemples/vault.exemple.yml`. `make config` (`config_proxmox.py`) écrit/édite
|
||
désormais cette voûte (semée depuis le gabarit si absente). `.gitignore` durci
|
||
(`**/vault.yml`). Docs mises à jour (config-proxmox.md avec étapes de migration,
|
||
intrants-communs.md §H, QUICKSTART). Le GUI ne stocke toujours aucun secret.
|
||
|
||
### Corrigé
|
||
- **Trois bugs trouvés au premier déploiement réel (cluster asgard).**
|
||
- **Clonage/Makefile codaient `inventories/lab` en dur** (config Proxmox + voûte) —
|
||
vestige du modèle env qui cassait les instances `principal`. Le playbook de clonage
|
||
et le Makefile détectent maintenant l'inventaire (`lab` > `principal` > `production`).
|
||
- **Redimensionnement disque non idempotent** : quand le disque dérivé du plan est
|
||
plus petit que le golden template, Proxmox refuse (`shrinking disks is not
|
||
supported`) et le clone échouait. Le resize est désormais **grow-only** (tolère le
|
||
cas, la VM garde le disque du template — le dérivé est un minimum).
|
||
- **PowerDNS refusait de démarrer** (`multiple backends 'bind'`) : le rôle
|
||
redéclarait `launch+=bind` que le paquet `pdns-backend-bind` pose déjà. Le rôle ne
|
||
déclare plus `launch` (seulement `bind-config`).
|
||
- **`chezlepro_timezone` n'était appliqué nulle part.** Cet intrant de base global
|
||
était défini mais aucun rôle ne s'en servait. Le rôle `chrony` (appliqué à tout
|
||
hôte via `serveur_debian`) règle désormais le fuseau horaire à partir de
|
||
`chezlepro_timezone` (`chrony_timezone` par défaut, vide = ne pas toucher). Les
|
||
autres défauts globaux (nœud / stockage / pont Proxmox) étaient déjà réutilisés
|
||
comme valeurs par défaut, surchargeables par hôte.
|
||
|
||
### Modifié
|
||
- **Détail GUI : section « Groupes (dérivés) » retirée (redondante).** Depuis la fusion
|
||
en atelier maître-détail, elle ne répétait que le socle (universel), les rôles des
|
||
applications (déjà dans « Applications ici ») et les intégrations (déjà cochées). Son
|
||
seul signal unique — le prérequis bloquant — est désormais nommé dans le pied
|
||
(« Prérequis manquant : … ») ; le panneau Dépendances reste pour la vue d'ensemble.
|
||
Code mort retiré (`renduGroupe`, `detailGroupe`, `titreGroupe`, CSS `.groupe*`).
|
||
- **Nettoyage CSS/HTML du GUI** après la refonte : retrait des règles et éléments
|
||
morts (`.message`, `.chips-filtre`/`.chip-f`, `.onglet*`, `.base-ligne`,
|
||
`.bases-liste`, `.ch-grp*`/`.ch-fleche`/`.ch-roles`, `.champ-val`, divs `#message`
|
||
et `#chips`).
|
||
- **GUI refondu en atelier maître-détail unifié.** Toutes les vues suivent le même
|
||
motif : tuiles à gauche, **détail + saisie à droite**, le panneau droit reflétant
|
||
la sélection de la vue courante (fin du panneau « figé » au changement de vue).
|
||
Les vues **Applications** et **Bases** passent de tableaux pleine largeur à ce
|
||
motif ; la saisie se fait dans le panneau droit, avec **liens cliquables** entre
|
||
objets (serveur → application → base). Le panneau droit est élargi (~38 %).
|
||
- **Fusion Serveur/Hôte.** Les vues « Inventaire » (hôtes, lecture seule) et
|
||
« Serveurs » (plan) faisaient doublon : elles sont fusionnées en **une seule vue
|
||
Serveurs**. Sa tuile porte le statut de réconciliation (réconcilié / divergent /
|
||
non instancié) ; son détail réunit l'**identité éditable** (plan), les **dérivés**
|
||
(VMID/IP/VLAN), les **groupes**, les **applications et bases hébergées**, et
|
||
**Vérifier/Déployer**. La navigation clavier (`1-3`, `j/k`, `v/d`, `/`) et le
|
||
filtre opèrent désormais sur les serveurs. Un bandeau « Comment lire ce parc »
|
||
explicite le modèle serveur·hôte·groupe·application·base. Code mort retiré
|
||
(`carte`, `renduChaine`, `ONGLETS`, `champLecture`, sélection d'hôte, etc.).
|
||
|
||
### Ajouté
|
||
- **Fluidité d'exploitation du GUI** (sans dépendance, stdlib pure) :
|
||
- **Notifications empilées (toasts)** auto-effaçables au lieu d'une bannière unique
|
||
écrasée ; les erreurs restent plus longtemps, les opérations longues mettent à
|
||
jour leur propre toast.
|
||
- **Durée d'exécution** affichée en direct dans la console (Vérifier / Déployer) et
|
||
suivi vivant de « Appliquer le plan ».
|
||
- **Navigation clavier** : `1-5` changent de vue, `j/k` parcourent les hôtes,
|
||
`v`/`d` vérifient/déploient l'hôte sélectionné, `/` cible le filtre, `Ctrl+S`
|
||
sauvegarde la vue éditable courante (ignorés pendant la saisie).
|
||
- **Validation inline** des champs du plan (vue Serveurs) : nom `fonction-NN`,
|
||
mémoire, cœurs, disque — liseré rouge et blocage avant l'envoi.
|
||
- **Garde-fou anti-perte** : confirmation `beforeunload` si des éditions de plan ne
|
||
sont pas sauvegardées.
|
||
- Le bouton **Sauvegarder** de l'en-tête devient contextuel (sauve la vue éditable,
|
||
désactivé en lecture seule) — fin de l'impasse 409 ; suppression du code mort.
|
||
|
||
### Ajouté
|
||
- **Vue Serveurs : champs alimentés par les paramètres globaux.** À `+ Serveur`,
|
||
**Nœud** et **Stockage** deviennent des listes déroulantes (catalogues
|
||
`proxmox_noeuds` / `proxmox_stockages`, vide = défaut global) et **Intégrations**
|
||
une rangée de cases à cocher des rôles `client_*` disponibles (au lieu d'une saisie
|
||
texte). Les catalogues Nœuds / Stockages / Ponts sont de nouveaux intrants
|
||
(classe « Catalogue ») éditables dans le panneau « Intrants de base » et stockés
|
||
dans `group_vars/proxmox.yml` ; l'API expose `integrations_disponibles` (scan de
|
||
`roles/client_*`). Le type `liste` (chaîne virgulée → liste YAML dédoublonnée) est
|
||
ajouté au schéma des intrants.
|
||
|
||
### Supprimé
|
||
- **Vue « Chaîne » du bandeau (redondante).** Son contenu (`renduChaine`) était déjà
|
||
rendu, par hôte, dans l'onglet « Chaîne » du détail de la vue Inventaire ; la vue ne
|
||
faisait que le répéter pour tous les hôtes à la fois, sans réseau/proxmox ni boutons
|
||
d'opération. Retirée (bouton, rendu `dessinerArbre`, CSS `.arbre-*`) ; la chaîne
|
||
reste consultable hôte par hôte. Raccourcis clavier ramenés à 1-4.
|
||
|
||
### Modifié
|
||
- **Vue Inventaire (cartes) : détail converti en inspection lecture seule.** La vue
|
||
affichait « généré depuis le plan (lecture seule) » tout en exposant une surface
|
||
d'édition (+ Hôte, ✕, bascule actif/planifié, ✨ Proposer, champs et cases de
|
||
groupes éditables) qui ne pouvait pas être persistée (l'inventaire est généré ;
|
||
`/api/inventaire` renvoie 409). Ces contrôles orphelins sont retirés : nom, champs
|
||
Réseau/Proxmox et groupes en lecture seule, état affiché en badge. **Vérifier /
|
||
Déployer** et les onglets d'inspection sont conservés. L'édition reste dans les vues
|
||
**Serveurs / Applications / Bases**. Code mort supprimé (`ajouterHote`,
|
||
`supprimerHote`, `definir`, `definirNom`, `definirEtat`, `basculerGroupe`,
|
||
`proposer`, `autoProposer`, `prochainSeqLibre`, `marquerModifie`, état `modifie`).
|
||
- **Référence des paramètres de `make config`** (`docs/config-proxmox.md`).
|
||
Explique un par un les 16 paramètres Proxmox non sensibles + les secrets API
|
||
demandés par l'assistant : sens, valeur par défaut, quoi saisir, et quels champs
|
||
sont des constantes vs des défauts surchargeables par hôte. Renvois ajoutés depuis
|
||
`make help` et `QUICKSTART.md`. Comble un trou : ces invites n'étaient expliquées
|
||
nulle part de façon pérenne (impératif « exploitable sans IA »).
|
||
- **Panneau « Intrants de base » dans le GUI.** Un bouton ⚙ Intrants ouvre une fenêtre
|
||
unique pour saisir les valeurs communes à tout l'écosystème, avec une distinction
|
||
visible entre **constantes** (valeur unique, non surchargeable) et **défauts**
|
||
(valeurs proposées, surchargeables dans les instances). Les secrets ne sont **jamais**
|
||
saisis ni affichés ici (garde-fou `INTRANTS_CLES_INTERDITES` + filtrage par schéma) :
|
||
ils restent dans le Vault Ansible, édités en CLI ; le panneau les liste seulement à
|
||
titre informatif. Un changement de `domaine_interne` (clé de voûte) demande une
|
||
confirmation explicite ; un `domaine_interne` vide est refusé côté serveur. La
|
||
nomenclature reste en lecture seule (modifiable dans le plan). Voir
|
||
`docs/intrants-communs.md` et `docs/intrants-base-gui-conception.md`.
|
||
- **Source unique d'identité partagée.** `domaine_interne` et `chezlepro_timezone`
|
||
vivent désormais dans `inventories/partage/intrants-identite.yml`, référencé par
|
||
symlink depuis chaque environnement (`group_vars/all/10-intrants.yml`) — fin de la
|
||
duplication lab/production.
|
||
- **Info-bulles d'aide sur les champs du GUI.** Survoler la description d'un champ à
|
||
saisir affiche une bulle avec des instructions sommaires.
|
||
|
||
## 2026-06-28
|
||
|
||
### Ajouté
|
||
- **Document de présentation de l'écosystème Chezlepro** (`docs/ecosysteme-chezlepro.md`).
|
||
Description vulgarisée à destination client/partenaire : les piliers de l'écosystème
|
||
souverain, le modèle reproductible (plan → génération → clonage → conformité), et un
|
||
inventaire des **mesures de renforcement** réellement en place (SSH durci, fail2ban,
|
||
sysctl, nftables, AppArmor, auditd, mises à jour automatiques, gestion des secrets,
|
||
garde-fous destructifs, sécurité par l'architecture). Honnête sur le statut
|
||
« défini/validé vs déployé ».
|
||
|
||
### Ajouté
|
||
- **Dimensionnement dérivé des ressources VM.** Les cœurs/RAM/disque d'une VM sont
|
||
désormais **estimés depuis les logiciels hébergés + le socle SE**, au lieu d'hériter
|
||
des specs du golden template (CPU/RAM identiques pour toutes les VM auparavant).
|
||
Chaque rôle déclare son empreinte (`roles/<rôle>/meta/empreinte.yml`) ; le
|
||
générateur somme par hôte (marge + arrondis) et écrit `proxmox_coeurs`/
|
||
`proxmox_memoire`/`proxmox_disque_taille` ; le clonage passe `cores`/`memory` à
|
||
Proxmox (`omit` si absent → aucune régression). Override par hôte possible dans le
|
||
plan (`serveurs.yml`). Voir `docs/dimensionnement-ressources.md`.
|
||
|
||
### Corrigé
|
||
- **`make instancier-appliquer FORCE=1` n'honorait pas `--force`.** La recette Makefile lançait `instancier.py appliquer` sans relayer `FORCE` ; le message « Utilise FORCE=1 » était donc trompeur (un changement de plan intentionnel restait bloqué). La recette passe désormais `$(if $(FORCE),--force)`. Trouvé en dogfooding.
|
||
|
||
## 2026-06-24 — Première publication publique
|
||
|
||
Première mise à disposition publique de **Set-OPS**, moteur Ansible d'écosystèmes
|
||
numériques souverains sur Proxmox — offert à la communauté québécoise par
|
||
l'**Alliance Boréale**, à la Saint-Jean-Baptiste 2026.
|
||
|
||
- **Moteur générique, piloté par un plan déclaratif** : on édite le plan
|
||
(`instance/plan/*.yml`), l'inventaire Ansible se génère, les VM se clonent depuis
|
||
un golden template Debian 13, les rôles s'appliquent par groupes. Tout passe par
|
||
`make`, le GUI local et la documentation.
|
||
- **Piliers d'un écosystème souverain** : socle Debian durci, AC/PKI interne, DNS
|
||
interne, identité (LDAP + SSO), relais courriel, bases de données, observabilité,
|
||
forge.
|
||
- **Catalogue de modèles prêts à déployer** (`exemples/modeles/`) : un hébergeur
|
||
copie un modèle, le renseigne à ses couleurs, et instancie.
|
||
- **Souveraineté jusqu'au bout** : Set-OPS s'exploite entièrement à la main, sans
|
||
aucune IA.
|
||
|
||
Pour démarrer : **`QUICKSTART.md`**.
|