site : quatre zones d'autorite, et l'ordre inscrit dans les integrations
Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite,
genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee,
mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP
sur une machine de tenant. La plus autoritaire etait la moins protegee.
Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis.
90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination
nommee — ce qui etait gratuit devient police.
L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ;
sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans
cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque
integration declare desormais sa dependance dans meta/integration.yml, les playbooks en
sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une
reponse valable : client_metrique pose un exportateur qu'on vient LIRE.
Autres defauts du meme soir :
- le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM :
boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable.
- dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur.
- l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre.
ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de
VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon
interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu`
reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur.
44 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
158c3b314a
commit
1a7c042e5b
15 changed files with 391 additions and 40 deletions
67
CHANGELOG.md
67
CHANGELOG.md
|
|
@ -1,5 +1,72 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-25 — Le SITE en quatre zones, et l'ordre inscrit dans les intégrations
|
||||
|
||||
**44 preuves.** Le site n'est plus un `/24` plat : une zone par **nature d'autorité**,
|
||||
chacune son VLAN et sa patte sur la frontière.
|
||||
|
||||
```
|
||||
opt3 vlan031 10.0.31.0/24 pilotage site-ops-01 voûte underlay, API Proxmox + OPNsense
|
||||
opt4 vlan032 10.0.32.0/24 autorite site-pki-01 racine de confiance
|
||||
opt5 vlan033 10.0.33.0/24 genome site-forge-01, site-cache-01
|
||||
opt6 vlan034 10.0.34.0/24 service site-dns-01
|
||||
```
|
||||
|
||||
L'inversion que ça corrige, mesurée : les cinq VM du site n'avaient **aucun** filtrage
|
||||
est-ouest — `enable=None`, `policy_in=None`, zéro règle — contre `enable=1 policy_in=DROP`
|
||||
sur une machine de tenant. **La machine la plus autoritaire du système était la moins
|
||||
protégée.** Le filtrage est nord-sud, par choix de l'exploitant : un seul point de police,
|
||||
un seul langage, un seul devis — plutôt que deux politiques à tenir d'accord.
|
||||
|
||||
Le devis passe de 90 à **117 règles**, et c'est tout le sens du découpage : *ce qui était
|
||||
gratuit devient policé*. Chaque flux `flotte` produit une règle **par zone source**, avec
|
||||
une destination nommée — le rôle visé, jamais la zone entière.
|
||||
|
||||
### L'ordre fait partie de l'intégration
|
||||
|
||||
`client_pki` s'exécutait sur tous les hôtes en parallèle. Sur celui qui porte l'autorité,
|
||||
le rôle réémet le certificat de `step-ca` et **recharge le service** ; les quatre autres
|
||||
l'interrogeaient dans cette fenêtre et échouaient ensemble sur `TLS handshake timeout` —
|
||||
un message qui accuse le réseau alors que la cause est une course de dix secondes.
|
||||
|
||||
Ce n'est pas propre à la PKI. Chaque intégration déclare désormais sa dépendance dans
|
||||
`meta/integration.yml`, les playbooks de groupe en sont le **miroir généré**, et **P44**
|
||||
refuse l'écart. Quatre contrôles négatifs : play unique, second play qui n'exclut pas le
|
||||
serveur, `any_errors_fatal` retiré, déclaration absente.
|
||||
|
||||
`serveur: ~` est une réponse, pas un oubli — `client_metrique` pose un exportateur que
|
||||
Prometheus vient *lire*. Toutes les intégrations ne dépendent pas d'un service debout.
|
||||
|
||||
*Le cas dangereux n'est pas le certificat mais le **résolveur** : un hôte basculé sur un
|
||||
résolveur pas encore prêt devient muet, et le runner qui devrait le réparer tombe avec lui.*
|
||||
|
||||
### Le plancher avant le premier `apt`
|
||||
|
||||
`hosts_statiques` venait après `common_packages`. Or apt vise le cache **par son nom** —
|
||||
ce qui est voulu. Le jour où les adresses changent, la boucle se referme : apt échoue faute
|
||||
de résoudre, donc le socle n'atteint jamais le plancher, donc le plancher reste périmé.
|
||||
Cinq machines bloquées, `/etc/hosts` vide. Le plancher **ne s'installe pas, il rend
|
||||
installable** — il passe donc avant.
|
||||
|
||||
### `dns_amorcage` se dérive
|
||||
|
||||
Écrit en dur, il a eu tort deux fois : la frontière d'abord, puis une adresse que le
|
||||
découpage a déplacée. Il se lit maintenant sur l'hôte qui porte `serveur_resolveur`.
|
||||
|
||||
Et l'ACL du résolveur dérivait de `setops_supernet` — juste tant que le site était plat.
|
||||
Découpé, il refusait trois zones sur quatre, *et la panne ressemble à un DNS mort alors
|
||||
que c'est une autorisation*.
|
||||
|
||||
### Ce que je me suis trompé à croire
|
||||
|
||||
J'ai diagnostiqué un trou noir de MTU et déclaré 1450 sur les quatre zones. **C'était
|
||||
faux.** Il n'y a pas de VXLAN sur ce chemin — il sert aux zones EVPN des tenants — et tout
|
||||
est à 1500 de bout en bout, vérifié sur `igb1`, les `vlan03x`, `bond3`, `vmbr3` et les
|
||||
cartes. Ma mesure (`1500 → échec`, `1300 → 200`) était réelle, mon interprétation ne
|
||||
l'était pas : je venais de débrancher la carte à chaud en modifiant `net0`, et chaque
|
||||
`ip link set mtu` **reconfigurait l'interface** au passage. C'est la reconfiguration qui
|
||||
débloquait, pas la valeur. Les VM sont en `mtu=1`, héritant du pont, à 1500.
|
||||
|
||||
## 2026-08-25 — P43 : la frontière voit-elle les machines du site ?
|
||||
|
||||
**43 preuves.** Celle-ci couvre ce qui a failli coûter 36 objets ce soir : le devis de la
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/production/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (43 OK · 0 echec · 0 saute)
|
||||
- **Verdict** : ✅ CONFORME (44 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -35,8 +35,8 @@
|
|||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. |
|
||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 8 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 86 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,10.29.19.41/32,192.168.254.2/32,192.168.255.2/32. |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 11 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 50 groupe(s), 79 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 5 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
|
|
@ -55,7 +55,8 @@
|
|||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 45 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 5 machine(s) du plan retrouvees, 21 regle(s) du site. |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 5 machine(s) du plan retrouvees, 56 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,38 @@
|
|||
---
|
||||
- name: Appliquer le groupe client_artefacts
|
||||
hosts: client_artefacts
|
||||
# client_artefacts — L'ORDRE FAIT PARTIE DE L'INTEGRATION (2026-08-25).
|
||||
#
|
||||
# Cette integration depend de `serveur_artefacts`, et sa metadonnee le declare
|
||||
# (`roles/client_artefacts/meta/integration.yml`). L'hote qui PORTE ce serveur est donc
|
||||
# configure AVANT ceux qui s'y adressent — sinon les clients s'enrolent aupres d'un
|
||||
# service qui n'est pas encore la, ou qui redemarre au meme instant.
|
||||
#
|
||||
# Ce fichier est le miroir de la declaration ; la preuve P44 refuse tout ecart.
|
||||
#
|
||||
# `any_errors_fatal` sur le premier play : si le serveur n'a pas pu etre configure,
|
||||
# y raccrocher des clients ne produit que des echecs qui accusent le reseau.
|
||||
|
||||
- name: Intégration client_artefacts — le serveur d'abord
|
||||
hosts: client_artefacts:&serveur_artefacts
|
||||
become: true
|
||||
module_defaults:
|
||||
any_errors_fatal: true
|
||||
module_defaults: &defauts_artefacts
|
||||
ansible.builtin.apt:
|
||||
lock_timeout: 300
|
||||
|
||||
gather_facts: true
|
||||
|
||||
pre_tasks:
|
||||
pre_tasks: &pre_artefacts
|
||||
- name: Vérifier que la cible est Debian
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.distribution == "Debian"
|
||||
fail_msg: "Ce playbook est prévu pour Debian."
|
||||
|
||||
roles:
|
||||
- client_artefacts
|
||||
|
||||
- name: Intégration client_artefacts — puis les hôtes qui s'y adressent
|
||||
hosts: client_artefacts:!serveur_artefacts
|
||||
become: true
|
||||
module_defaults: *defauts_artefacts
|
||||
gather_facts: true
|
||||
pre_tasks: *pre_artefacts
|
||||
roles:
|
||||
- client_artefacts
|
||||
|
|
|
|||
|
|
@ -1,22 +1,38 @@
|
|||
---
|
||||
- name: Appliquer le groupe client_journal
|
||||
hosts: client_journal
|
||||
# client_journal — L'ORDRE FAIT PARTIE DE L'INTEGRATION (2026-08-25).
|
||||
#
|
||||
# Cette integration depend de `serveur_loki`, et sa metadonnee le declare
|
||||
# (`roles/client_journal/meta/integration.yml`). L'hote qui PORTE ce serveur est donc
|
||||
# configure AVANT ceux qui s'y adressent — sinon les clients s'enrolent aupres d'un
|
||||
# service qui n'est pas encore la, ou qui redemarre au meme instant.
|
||||
#
|
||||
# Ce fichier est le miroir de la declaration ; la preuve P44 refuse tout ecart.
|
||||
#
|
||||
# `any_errors_fatal` sur le premier play : si le serveur n'a pas pu etre configure,
|
||||
# y raccrocher des clients ne produit que des echecs qui accusent le reseau.
|
||||
|
||||
- name: Intégration client_journal — le serveur d'abord
|
||||
hosts: client_journal:&serveur_loki
|
||||
become: true
|
||||
# Le verrou dpkg est tenu par les maj automatiques de Debian, par vagues, plusieurs
|
||||
# minutes apres le premier demarrage. Pose ICI plutot que dans chaque role : toute
|
||||
# tache apt du play en herite, y compris celles des roles inclus.
|
||||
module_defaults:
|
||||
any_errors_fatal: true
|
||||
module_defaults: &defauts_journal
|
||||
ansible.builtin.apt:
|
||||
lock_timeout: 300
|
||||
|
||||
gather_facts: true
|
||||
|
||||
pre_tasks:
|
||||
pre_tasks: &pre_journal
|
||||
- name: Vérifier que la cible est Debian
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.distribution == "Debian"
|
||||
fail_msg: "Ce playbook est prévu pour Debian."
|
||||
|
||||
roles:
|
||||
- client_journal
|
||||
|
||||
- name: Intégration client_journal — puis les hôtes qui s'y adressent
|
||||
hosts: client_journal:!serveur_loki
|
||||
become: true
|
||||
module_defaults: *defauts_journal
|
||||
gather_facts: true
|
||||
pre_tasks: *pre_journal
|
||||
roles:
|
||||
- client_journal
|
||||
|
|
|
|||
|
|
@ -1,22 +1,38 @@
|
|||
---
|
||||
- name: Appliquer le groupe client_pki
|
||||
hosts: client_pki
|
||||
# client_pki — L'ORDRE FAIT PARTIE DE L'INTEGRATION (2026-08-25).
|
||||
#
|
||||
# Cette integration depend de `serveur_step_ca`, et sa metadonnee le declare
|
||||
# (`roles/client_pki/meta/integration.yml`). L'hote qui PORTE ce serveur est donc
|
||||
# configure AVANT ceux qui s'y adressent — sinon les clients s'enrolent aupres d'un
|
||||
# service qui n'est pas encore la, ou qui redemarre au meme instant.
|
||||
#
|
||||
# Ce fichier est le miroir de la declaration ; la preuve P44 refuse tout ecart.
|
||||
#
|
||||
# `any_errors_fatal` sur le premier play : si le serveur n'a pas pu etre configure,
|
||||
# y raccrocher des clients ne produit que des echecs qui accusent le reseau.
|
||||
|
||||
- name: Intégration client_pki — le serveur d'abord
|
||||
hosts: client_pki:&serveur_step_ca
|
||||
become: true
|
||||
# Le verrou dpkg est tenu par les maj automatiques de Debian, par vagues, plusieurs
|
||||
# minutes apres le premier demarrage. Pose ICI plutot que dans chaque role : toute
|
||||
# tache apt du play en herite, y compris celles des roles inclus.
|
||||
module_defaults:
|
||||
any_errors_fatal: true
|
||||
module_defaults: &defauts_pki
|
||||
ansible.builtin.apt:
|
||||
lock_timeout: 300
|
||||
|
||||
gather_facts: true
|
||||
|
||||
pre_tasks:
|
||||
pre_tasks: &pre_pki
|
||||
- name: Vérifier que la cible est Debian
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.distribution == "Debian"
|
||||
fail_msg: "Ce playbook est prévu pour Debian."
|
||||
|
||||
roles:
|
||||
- client_pki
|
||||
|
||||
- name: Intégration client_pki — puis les hôtes qui s'y adressent
|
||||
hosts: client_pki:!serveur_step_ca
|
||||
become: true
|
||||
module_defaults: *defauts_pki
|
||||
gather_facts: true
|
||||
pre_tasks: *pre_pki
|
||||
roles:
|
||||
- client_pki
|
||||
|
|
|
|||
|
|
@ -1,15 +1,38 @@
|
|||
---
|
||||
- name: Appliquer le groupe client_resolveur
|
||||
hosts: client_resolveur
|
||||
become: true
|
||||
gather_facts: true
|
||||
# client_resolveur — L'ORDRE FAIT PARTIE DE L'INTEGRATION (2026-08-25).
|
||||
#
|
||||
# Cette integration depend de `serveur_resolveur`, et sa metadonnee le declare
|
||||
# (`roles/client_resolveur/meta/integration.yml`). L'hote qui PORTE ce serveur est donc
|
||||
# configure AVANT ceux qui s'y adressent — sinon les clients s'enrolent aupres d'un
|
||||
# service qui n'est pas encore la, ou qui redemarre au meme instant.
|
||||
#
|
||||
# Ce fichier est le miroir de la declaration ; la preuve P44 refuse tout ecart.
|
||||
#
|
||||
# `any_errors_fatal` sur le premier play : si le serveur n'a pas pu etre configure,
|
||||
# y raccrocher des clients ne produit que des echecs qui accusent le reseau.
|
||||
|
||||
pre_tasks:
|
||||
- name: Intégration client_resolveur — le serveur d'abord
|
||||
hosts: client_resolveur:&serveur_resolveur
|
||||
become: true
|
||||
any_errors_fatal: true
|
||||
module_defaults: &defauts_resolveur
|
||||
ansible.builtin.apt:
|
||||
lock_timeout: 300
|
||||
gather_facts: true
|
||||
pre_tasks: &pre_resolveur
|
||||
- name: Vérifier que la cible est Debian
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.distribution == "Debian"
|
||||
fail_msg: "Ce playbook est prévu pour Debian."
|
||||
|
||||
roles:
|
||||
- client_resolveur
|
||||
|
||||
- name: Intégration client_resolveur — puis les hôtes qui s'y adressent
|
||||
hosts: client_resolveur:!serveur_resolveur
|
||||
become: true
|
||||
module_defaults: *defauts_resolveur
|
||||
gather_facts: true
|
||||
pre_tasks: *pre_resolveur
|
||||
roles:
|
||||
- client_resolveur
|
||||
|
|
|
|||
|
|
@ -78,8 +78,21 @@
|
|||
| select('in', serveur_debian_resolv_texte) | list | length == 0
|
||||
|
||||
roles:
|
||||
- common_packages
|
||||
# LE PLANCHER AVANT LE PREMIER `apt` (2026-08-25).
|
||||
#
|
||||
# `hosts_statiques` venait APRÈS `common_packages`, qui commence par un `apt update`.
|
||||
# Or apt vise le cache PAR SON NOM — `site-cache-01.<domaine>` — ce qui est voulu :
|
||||
# une configuration agnostique de l'adressage.
|
||||
#
|
||||
# Le jour où les adresses changent, la boucle se referme : apt échoue faute de
|
||||
# résoudre, donc le socle n'atteint jamais `hosts_statiques`, donc le plancher reste
|
||||
# périmé, donc apt échoue. Mesuré le 2026-08-25 après le découpage du site en zones :
|
||||
# cinq machines bloquées, `/etc/hosts` vide de toute entrée du site.
|
||||
#
|
||||
# Le résolveur d'amorçage est déjà une `pre_task` pour exactement cette raison. Le
|
||||
# plancher est de la même nature : il ne s'installe pas, **il rend installable**.
|
||||
- hosts_statiques
|
||||
- common_packages
|
||||
- qemu_guest_agent
|
||||
- cloud_init
|
||||
- sudo_ansible
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le
|
||||
# raisonnement, et docs/decisions-architecture.md (D-33).
|
||||
integration:
|
||||
# DE QUI CETTE INTEGRATION DEPEND, ET POURQUOI L'ORDRE COMPTE.
|
||||
#
|
||||
# Braquer apt sur un cache absent casse toute installation ulterieure — y compris celle
|
||||
# des roles qui suivent dans le meme deploiement.
|
||||
#
|
||||
# Le playbook de groupe applique l'hote qui porte ce serveur AVANT ceux qui s'y
|
||||
# adressent, et la preuve P44 refuse tout ecart entre cette declaration et lui.
|
||||
serveur: serveur_artefacts
|
||||
universelle: true
|
||||
raison: >-
|
||||
Tout hote installe des paquets, et doit les prendre chez lui quand l'ecosysteme a
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le
|
||||
# raisonnement, et docs/decisions-architecture.md (D-33).
|
||||
integration:
|
||||
# DE QUI CETTE INTEGRATION DEPEND, ET POURQUOI L'ORDRE COMPTE.
|
||||
#
|
||||
# Un collecteur qui expedie vers un puits inexistant accumule en local, sans le dire.
|
||||
#
|
||||
# Le playbook de groupe applique l'hote qui porte ce serveur AVANT ceux qui s'y
|
||||
# adressent, et la preuve P44 refuse tout ecart entre cette declaration et lui.
|
||||
serveur: serveur_loki
|
||||
universelle: true
|
||||
raison: >-
|
||||
Tout hote expedie ses journaux. Un incident se reconstitue depuis les traces
|
||||
|
|
|
|||
|
|
@ -3,6 +3,15 @@
|
|||
# serveur du plan : une ligne recopiee 14 fois n'existe que pour etre oubliee une
|
||||
# quinzieme. Voir docs/integrations-vm.md et docs/decisions-architecture.md (D-33).
|
||||
integration:
|
||||
# DE QUI CETTE INTEGRATION DEPEND, ET POURQUOI L'ORDRE COMPTE.
|
||||
#
|
||||
# AUCUNE. `client_metrique` pose un exportateur que Prometheus vient LIRE : il n'attend
|
||||
# personne, c'est le serveur qui vient a lui. La distinction compte — toutes les
|
||||
# integrations ne dependent pas d'un service debout.
|
||||
#
|
||||
# Le playbook de groupe applique l'hote qui porte ce serveur AVANT ceux qui s'y
|
||||
# adressent, et la preuve P44 refuse tout ecart entre cette declaration et lui.
|
||||
serveur: ~
|
||||
universelle: true
|
||||
raison: >-
|
||||
Tout hote est mesure. Une machine hors supervision tombe sans que personne
|
||||
|
|
|
|||
|
|
@ -2,6 +2,16 @@
|
|||
# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le
|
||||
# raisonnement, et docs/decisions-architecture.md (D-33).
|
||||
integration:
|
||||
# DE QUI CETTE INTEGRATION DEPEND, ET POURQUOI L'ORDRE COMPTE.
|
||||
#
|
||||
# L'AC doit exister ET repondre avant qu'un hote lui demande un certificat. Sur l'hote
|
||||
# qui la porte, le role recharge `step-ca` en reemettant son propre certificat :
|
||||
# pendant cette fenetre, elle ne repond plus. Mesure du 2026-08-25 : quatre clients en
|
||||
# echec simultane, message `TLS handshake timeout` — qui accuse le reseau, pas l'ordre.
|
||||
#
|
||||
# Le playbook de groupe applique l'hote qui porte ce serveur AVANT ceux qui s'y
|
||||
# adressent, et la preuve P44 refuse tout ecart entre cette declaration et lui.
|
||||
serveur: serveur_step_ca
|
||||
universelle: true
|
||||
raison: >-
|
||||
Tout hote fait confiance a l'AC interne et porte un certificat : c'est le
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le
|
||||
# raisonnement, et docs/decisions-architecture.md (D-33).
|
||||
integration:
|
||||
# DE QUI CETTE INTEGRATION DEPEND, ET POURQUOI L'ORDRE COMPTE.
|
||||
#
|
||||
# Basculer un hote sur un resolveur qui n'est pas encore pret le rend MUET, et avec lui
|
||||
# tout ce qui resout un nom. C'est la dependance la plus dangereuse du lot : le runner
|
||||
# qui devrait reparer tombe avec les autres.
|
||||
#
|
||||
# Le playbook de groupe applique l'hote qui porte ce serveur AVANT ceux qui s'y
|
||||
# adressent, et la preuve P44 refuse tout ecart entre cette declaration et lui.
|
||||
serveur: serveur_resolveur
|
||||
universelle: true
|
||||
raison: >-
|
||||
Tout hote resout des noms, et doit le faire aupres du resolveur de son ecosysteme
|
||||
|
|
|
|||
|
|
@ -330,6 +330,22 @@ def _machines_du_plan_site() -> list[dict]:
|
|||
if str(s.get("etat", "actif")) == "actif"]
|
||||
|
||||
|
||||
def _ports_du_plan_site() -> dict[str, str]:
|
||||
"""Port declare au plan pour chaque role du site — ce que `port: derive` vaut ici.
|
||||
|
||||
UN PORT NON NUMERIQUE N'EST PAS UNE ECOUTE (voir `resoudre_flux`). `derive` dit que le
|
||||
port depend du deploiement : la forge ecoute 3000 derriere un edge, 443 quand elle
|
||||
sert son propre TLS. Le rendre tel quel produirait une regle que le boitier refuse —
|
||||
on l'a deja paye sur le pare-feu est-ouest.
|
||||
"""
|
||||
apps = (_lire_plan_site("applications.yml") or {}).get("applications") or {}
|
||||
out: dict[str, str] = {}
|
||||
for app in apps.values():
|
||||
if app.get("groupe") and app.get("port"):
|
||||
out[str(app["groupe"])] = str(app["port"])
|
||||
return out
|
||||
|
||||
|
||||
def _services_du_plan_site() -> dict[str, list[str]]:
|
||||
apps = (_lire_plan_site("applications.yml") or {}).get("applications") or {}
|
||||
out: dict[str, list[str]] = {}
|
||||
|
|
@ -348,6 +364,14 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
if_transit = _ou_marqueur(_intr, "opnsense_if_transit", IF_TRANSIT)
|
||||
if_gestion = _ou_marqueur(_intr, "opnsense_if_gestion", IF_GESTION)
|
||||
if_site = _ou_marqueur(_intr, "opnsense_if_site", IF_SITE)
|
||||
# UNE PATTE PAR ZONE DU SITE (2026-08-25). Le site n'est plus un `/24` plat : chaque
|
||||
# nature d'autorite a son VLAN, et le trafic penetre le pare-feu par SA patte. Une
|
||||
# regle posee sur la mauvaise interface ne correspond jamais.
|
||||
if_zones = dict(_intr.get("opnsense_if_zones") or {})
|
||||
|
||||
def _if_de(reseau: str | None) -> str:
|
||||
"""L'interface ou arrive le trafic de cette zone ; l'ancienne patte a defaut."""
|
||||
return if_zones.get(str(reseau or ""), if_site)
|
||||
nom_actif, nomenclature_active = _instance_active()
|
||||
|
||||
# L'alias d'un tenant vaut ses SOUS-RESEAUX ATTRIBUES, pas son supernet.
|
||||
|
|
@ -596,6 +620,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
# attrape, et rien d'autre ne l'aurait fait.
|
||||
_machines_site = _machines_du_plan_site()
|
||||
_services_site = _services_du_plan_site()
|
||||
_ports_site = _ports_du_plan_site()
|
||||
if _machines_site:
|
||||
_res_site = {r.get("nom"): r for r in underlay_mod.reseaux(_u)}
|
||||
_sous_site = sorted({str(_res_site[m["reseau"]]["sous_reseau"])
|
||||
|
|
@ -687,6 +712,24 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
# parce qu'une regle porte une source.
|
||||
_portees = [(f"SETOPS_TENANT_{p2}{n2['index']}", if_transit)
|
||||
for _n2, p2, n2 in tenants]
|
||||
elif _entrant and "flotte" in _pairs_fl:
|
||||
# CE QUI ETAIT GRATUIT DEVIENT POLICE (2026-08-25).
|
||||
#
|
||||
# `flotte` veut dire « les machines de mon ecosysteme ». Dans un `/24`
|
||||
# plat, ce trafic ne traversait rien : le cache, la forge, le
|
||||
# resolveur et l'AC se joignaient en L2, sans qu'aucune regle
|
||||
# n'existe — ni ne manque.
|
||||
#
|
||||
# Une zone par autorite change cela : une machine qui interroge le
|
||||
# resolveur ou tire un paquet du cache passe desormais par la
|
||||
# frontiere. Ce qui n'avait pas besoin d'etre dit doit l'etre.
|
||||
#
|
||||
# Une regle PAR ZONE SOURCE : le paquet penetre le pare-feu par la
|
||||
# patte de la zone d'ou il part, et une regle posee ailleurs ne
|
||||
# correspondrait jamais. La destination reste nommee — le role vise,
|
||||
# jamais la zone entiere.
|
||||
_portees = [("SETOPS_SITE", z) for z in
|
||||
sorted({_if_de(m.get("reseau")) for m in _machines_site})]
|
||||
elif _entrant and "22" in _ports(_fl) and _admin_site:
|
||||
# Le SSH de gestion arrive par la patte d'ADMINISTRATION de la
|
||||
# frontiere, pas par celle du site : c'est la que le paquet penetre
|
||||
|
|
@ -695,7 +738,25 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
elif _entrant:
|
||||
continue # rien d'autre n'entre chez le site depuis l'exterieur
|
||||
else:
|
||||
_portees = [(_a, if_site)]
|
||||
# Une portee par ZONE ou ce role est present : deux machines d'un meme
|
||||
# role dans deux zones differentes ne partagent pas leur patte.
|
||||
_zones = sorted({_if_de(m.get("reseau")) for m in _machines_site
|
||||
if _role == "serveur_debian"
|
||||
or _role in _services_site.get(m["nom"], [])})
|
||||
_portees = [(_a, z) for z in _zones] or [(_a, if_site)]
|
||||
# `derive` se resout par le port declare au plan ; sans lui, on n'emet
|
||||
# rien plutot qu'une regle que le boitier refusera.
|
||||
_p = _ports(_fl)
|
||||
if any(not str(x).isdigit() for x in _p):
|
||||
_reel = _ports_site.get(_role)
|
||||
if not _reel:
|
||||
# SUR STDERR, JAMAIS STDOUT : ce script rend du JSON, et une
|
||||
# note au milieu le rend illisible a `appliquer_opnsense`.
|
||||
print(f"note : {_role} declare un port `{'/'.join(_p)}` que le "
|
||||
f"plan du site ne resout pas — aucune regle emise.",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
_p = [_reel]
|
||||
for _src, _itf in _portees:
|
||||
regles.append({
|
||||
"sens": "in" if _entrant else "out",
|
||||
|
|
@ -704,7 +765,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
"source": _src if _entrant else _a,
|
||||
"destination": _a if _entrant else (
|
||||
"SETOPS_FABRIC" if "fabric" in _pairs_fl else "!SETOPS_INTERNES"),
|
||||
"ports": _ports(_fl),
|
||||
"ports": _p,
|
||||
"chiffrement": _fl.get("chiffrement"),
|
||||
"role": _role,
|
||||
"tenant": "SITE",
|
||||
|
|
|
|||
|
|
@ -1013,6 +1013,62 @@ def preuve_devis_frontiere_du_site() -> tuple[bool, str]:
|
|||
f"{n_site} regle(s) du site.")
|
||||
|
||||
|
||||
def preuve_ordre_des_integrations() -> tuple[bool, str]:
|
||||
"""Une integration applique SON SERVEUR avant ceux qui s'y adressent.
|
||||
|
||||
POURQUOI (2026-08-25). `client_pki` s'executait sur tous les hotes en parallele. Sur
|
||||
celui qui porte l'autorite, le role reemet le certificat de `step-ca` et RECHARGE le
|
||||
service ; les quatre autres l'interrogeaient dans cette fenetre et echouaient ensemble
|
||||
sur `net/http: TLS handshake timeout` — un message qui accuse le reseau alors que la
|
||||
cause est une course de dix secondes.
|
||||
|
||||
Ce n'est pas propre a la PKI. Toute integration a la meme forme : un `client_*`
|
||||
s'adresse a un `serveur_*`, et l'ordre compte. Le cas le plus dangereux n'est pas le
|
||||
certificat mais le RESOLVEUR — un hote bascule sur un resolveur pas encore pret
|
||||
devient muet, et le runner qui devrait le reparer tombe avec lui.
|
||||
|
||||
La dependance se declare dans `roles/<client>/meta/integration.yml` (`serveur:`), et
|
||||
le playbook de groupe en est le miroir. Cette preuve refuse l'ecart entre les deux :
|
||||
sans elle, la declaration deviendrait un commentaire, et l'ordre une coincidence.
|
||||
|
||||
`serveur: ~` est une reponse valable — `client_metrique` pose un exportateur que
|
||||
Prometheus vient LIRE, il n'attend personne. Toutes les integrations ne dependent pas
|
||||
d'un service debout, et le dire explicitement vaut mieux que l'omettre.
|
||||
"""
|
||||
manques: list[str] = []
|
||||
vus = 0
|
||||
for meta in sorted((RACINE / "roles").glob("client_*/meta/integration.yml")):
|
||||
role = meta.parent.parent.name
|
||||
d = (yaml.safe_load(meta.read_text(encoding="utf-8")) or {}).get("integration") or {}
|
||||
if "serveur" not in d:
|
||||
manques.append(f"{role} : `meta/integration.yml` ne declare pas `serveur:` — "
|
||||
f"on ne peut donc pas savoir si son ordre est correct "
|
||||
f"(mettre `~` si l'integration n'attend aucun service)")
|
||||
continue
|
||||
srv = d.get("serveur")
|
||||
pb = RACINE / "playbooks" / "groupes" / f"{role}.yml"
|
||||
if not pb.is_file():
|
||||
continue
|
||||
texte = pb.read_text(encoding="utf-8")
|
||||
if not srv:
|
||||
continue # rien a ordonner, et c'est declare
|
||||
vus += 1
|
||||
if f"hosts: {role}:&{srv}" not in texte:
|
||||
manques.append(f"{role} : declare dependre de `{srv}`, mais son playbook "
|
||||
f"n'applique pas d'abord `{role}:&{srv}`")
|
||||
if f"hosts: {role}:!{srv}" not in texte:
|
||||
manques.append(f"{role} : son playbook n'exclut pas `{srv}` du second play — "
|
||||
f"le serveur serait reconfigure deux fois, et l'ordre perdu")
|
||||
if "any_errors_fatal: true" not in texte:
|
||||
manques.append(f"{role} : sans `any_errors_fatal` sur le premier play, un "
|
||||
f"serveur en echec laisse ses clients s'y raccrocher pour rien")
|
||||
if manques:
|
||||
return False, ("L'ordre des integrations n'est pas garanti :\n - "
|
||||
+ "\n - ".join(manques))
|
||||
return True, (f"{vus} integration(s) appliquent leur serveur avant leurs clients.")
|
||||
|
||||
|
||||
|
||||
def preuve_glossaire_enseigne() -> tuple[bool, str]:
|
||||
"""Tout mot que le depot emploie devant l'exploitant est explique au glossaire.
|
||||
|
||||
|
|
@ -1351,6 +1407,8 @@ PREUVES: list[dict] = [
|
|||
"func": preuve_resolution_unique},
|
||||
{"id": "P42", "titre": "L'edge porte les noms qu'il publie", "refs": [],
|
||||
"func": preuve_edge_porte_ses_noms},
|
||||
{"id": "P44", "titre": "Integrations : le serveur avant ses clients", "refs": [],
|
||||
"func": preuve_ordre_des_integrations},
|
||||
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
||||
"func": preuve_devis_frontiere_du_site},
|
||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||
|
|
|
|||
|
|
@ -93,9 +93,32 @@ def inventaire() -> dict:
|
|||
return {"_meta": {"hostvars": {}}}
|
||||
|
||||
par_reseau = {r.get("nom"): r for r in U.reseaux(u)}
|
||||
# Les sous-reseaux ou vivent REELLEMENT des machines du site : l'etendue de
|
||||
# l'ecosysteme, telle que le plan la dessine.
|
||||
_zones_du_site = sorted({str((par_reseau.get(s.get("reseau")) or {}).get("sous_reseau"))
|
||||
for s in serveurs.values()
|
||||
if str(s.get("etat", "actif")) == "actif"
|
||||
and (par_reseau.get(s.get("reseau")) or {}).get("sous_reseau")})
|
||||
|
||||
# LE RESOLVEUR D'AMORCAGE SE DERIVE, IL NE S'ECRIT PAS (2026-08-25).
|
||||
#
|
||||
# Il a ete ecrit en dur deux fois, et il a eu tort les deux fois : d'abord la frontiere
|
||||
# (`10.0.3.1`) alors que le site avait son DNS, puis `10.0.3.51` — juste jusqu'a ce que
|
||||
# le decoupage en zones deplace la machine en `10.0.34.11`.
|
||||
#
|
||||
# Une adresse ecrite a la main est une copie ; une copie se perime. Celle-ci se lit
|
||||
# desormais la ou elle est vraie : l'hote qui porte `serveur_resolveur` dans ce plan.
|
||||
# Le plan peut encore la surcharger — pour l'amorcage d'un site tout neuf, ou le
|
||||
# resolveur n'existe pas encore — mais ce n'est plus le cas normal.
|
||||
_resolveur = next((str(s.get("ip")) for nom, s in serveurs.items()
|
||||
if any(a.get("hote") == nom and a.get("groupe") == "serveur_resolveur"
|
||||
for a in applications.values())
|
||||
and str(s.get("etat", "actif")) == "actif"), "")
|
||||
|
||||
# Ce que TOUTE machine du site recoit, des intrants du plan.
|
||||
communes: dict = {}
|
||||
if _resolveur:
|
||||
communes["dns_amorcage"] = _resolveur
|
||||
for cle in ("domaine_interne", "organisation", "dns_amorcage"):
|
||||
if intrants.get(cle):
|
||||
communes[cle] = str(intrants[cle])
|
||||
|
|
@ -188,6 +211,17 @@ def inventaire() -> dict:
|
|||
# le site le tient du reseau ou vivent ses machines. Meme sens, autre source.
|
||||
# `serveur_resolveur` s'en sert pour savoir QUI a le droit de l'interroger.
|
||||
"setops_supernet": str(r.get("sous_reseau") or ""),
|
||||
# QUI A LE DROIT D'INTERROGER LE RESOLVEUR — TOUTES LES ZONES DU SITE.
|
||||
#
|
||||
# `serveur_resolveur` derive sa liste d'autorisation de `setops_supernet`, qui
|
||||
# vaut le sous-reseau de la machine. C'etait juste tant que le site etait un
|
||||
# `/24` plat. Decoupe en zones, le resolveur n'autorisait plus que la SIENNE :
|
||||
# les trois autres se faisaient refuser, et la panne ressemble a un DNS mort
|
||||
# alors que c'est une ACL.
|
||||
#
|
||||
# Un resolveur ouvert au monde est un relais d'amplification ; celui-ci est
|
||||
# ouvert a son ecosysteme, et a rien d'autre.
|
||||
"serveur_resolveur_reseaux_autorises": _zones_du_site,
|
||||
# Un role partage peut avoir besoin de savoir de quel cote il tourne.
|
||||
"setops_site": True,
|
||||
# Le plan du site, pour les roles qui lisent des registres.
|
||||
|
|
|
|||
Loading…
Reference in a new issue