l heure vient de la frontiere : la derniere dependance vivante tombe
Mesure sur les quatorze : aucune sortie TCP vers une adresse publique, apt par le cache, noms autoritaires en local, unattended-upgrades masked. Et quatre pairs NTP publics par machine. Le role chrony posait le fuseau et installait le demon sans jamais toucher a ses sources : le defaut de Debian tenait depuis le premier jour, herite et jamais choisi. L autorite est la frontiere, par decision de l exploitant. Elle etait deja stratum 2 et ecoutait en 123 ; il ne manquait que le passage. 9 anciennes regles port 123 vers !SETOPS_INTERNES retirees, 9 regles nommees vers SETOPS_FRONTIERE posees : le changement resserre autant qu il centralise. Deux chemins parce que la topologie en a deux. Un tenant n atteint pas la frontiere par sa passerelle de zone — tenue par le SDN — mais par le lien de transit. Une machine du site a la frontiere pour passerelle directe. Les deux valeurs sont derivees de l underlay, via reseau_transit() plutot que d un prefixe d adresse qui aurait menti chez le prochain hebergeur. La patte face aux tenants manquait a opnsense_if_zones, pour la meme raison que grappe-controle la veille. Sonde horloge ecrite en meme temps : synchronisee ET contre la source DECLAREE. Une machine peut etre parfaitement a l heure contre quatre serveurs publics — c est exactement l etat d avant. 14 tenant + 7 site, toutes disciplinees, sources publiques = 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
989398f3cf
commit
6786d15068
15 changed files with 399 additions and 11 deletions
74
CHANGELOG.md
74
CHANGELOG.md
|
|
@ -1,5 +1,79 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-09-11 (4) — L'heure vient de la frontiere : la derniere dependance vivante tombe
|
||||||
|
|
||||||
|
L'exploitant a presume qu'un ecosysteme fonctionnel n'avait plus besoin d'Internet pour ses
|
||||||
|
operations internes. Mesure sur les quatorze machines : presque.
|
||||||
|
|
||||||
|
sorties TCP etablies vers une adresse publique aucune, sur 14/14
|
||||||
|
apt par le cache du site
|
||||||
|
noms internes autoritaires, en local
|
||||||
|
unattended-upgrades / apt-daily masked, masked, masked
|
||||||
|
|
||||||
|
pool 2.debian.pool.ntp.org iburst QUATRE pairs publics, sur les 14
|
||||||
|
|
||||||
|
Le role `chrony` posait le fuseau et INSTALLAIT le demon sans jamais toucher a ses sources.
|
||||||
|
Le defaut de Debian tenait depuis le premier jour : herite, jamais choisi.
|
||||||
|
|
||||||
|
### Pourquoi c'etait la mauvaise a laisser trainer
|
||||||
|
|
||||||
|
step-ca emet des certificats a duree courte ; TLS refuse un certificat pas encore valide ;
|
||||||
|
Loki rejette une ligne trop loin dans le futur ; les silences d'Icinga reposent sur des
|
||||||
|
`ttl`. Une flotte dont les horloges divergent tombe en panne DE L'INTERIEUR, avec des
|
||||||
|
symptomes qui ressemblent a tout sauf a une horloge.
|
||||||
|
|
||||||
|
Et la derive est lente : couper Internet ne casse rien le premier jour. Le systeme parait
|
||||||
|
souverain jusqu'a ce qu'il ne le soit plus, sans signal entre les deux.
|
||||||
|
|
||||||
|
### L'autorite est la frontiere — decision de l'exploitant
|
||||||
|
|
||||||
|
Elle etait deja `stratum 2`, synchronisee, et ecoutait en 123 sur chaque patte. Il ne
|
||||||
|
manquait que le passage : son `pf` est en refus par defaut et aucun flux NTP n'etait
|
||||||
|
declare vers elle.
|
||||||
|
|
||||||
|
a creer : 14 | a retirer : 9
|
||||||
|
|
||||||
|
Les NEUF retires sont les anciennes regles `port 123 -> !SETOPS_INTERNES` : le 123 etait
|
||||||
|
autorise vers N'IMPORTE QUELLE adresse d'Internet. **Le changement resserre autant qu'il
|
||||||
|
centralise.** La destination est desormais nommee, alias `SETOPS_FRONTIERE`.
|
||||||
|
|
||||||
|
### Deux chemins, parce que la topologie en a deux
|
||||||
|
|
||||||
|
Un tenant n'atteint PAS la frontiere par sa passerelle de zone : `10.17.x.1` est tenue par
|
||||||
|
le SDN de Proxmox. Il sort par le lien de transit — `10.0.4.1`, patte libellee TENANTS,
|
||||||
|
qui manquait a `opnsense_if_zones` pour la meme raison que `grappe-controle` la veille.
|
||||||
|
Une machine du site, elle, a la frontiere pour passerelle directe.
|
||||||
|
|
||||||
|
La valeur est DERIVEE des deux cotes, jamais ecrite : `instancier` prend la patte sur le
|
||||||
|
reseau de transit (via `reseau_transit()`, qui le derive de `passerelle_sortie` — le
|
||||||
|
reconnaitre a son prefixe d'adresse aurait marche ici et menti chez le prochain
|
||||||
|
hebergeur) ; `site_inventaire` prend la patte de la zone de la machine.
|
||||||
|
|
||||||
|
14 machines du tenant ^* 10.0.4.1 publiques=0
|
||||||
|
7 machines du site ^* 10.0.3x.1 ecarts en nanosecondes
|
||||||
|
|
||||||
|
### Le drop-in ajoute, il ne retire pas
|
||||||
|
|
||||||
|
`/etc/chrony/conf.d/` est lu EN PLUS du fichier principal. Y declarer notre serveur sans
|
||||||
|
neutraliser le `pool` de Debian aurait laisse cinq sources, dont quatre sur Internet —
|
||||||
|
l'horloge centralisee en apparence, la dependance intacte.
|
||||||
|
|
||||||
|
### La sonde `horloge`, ecrite en meme temps
|
||||||
|
|
||||||
|
Elle mesure DEUX choses, et la seconde est celle qui manquait : la machine est-elle
|
||||||
|
synchronisee, ET contre la source DECLAREE ? Une machine peut etre parfaitement a l'heure
|
||||||
|
en interrogeant quatre serveurs publics — c'est exactement l'etat d'avant, et mesurer la
|
||||||
|
seule derive l'aurait laisse invisible.
|
||||||
|
|
||||||
|
Le seuil d'ecart (500 ms) est tres large, et c'est voulu : il doit crier sur une horloge qui
|
||||||
|
DECROCHE, pas sur la respiration d'un `chronyd`. L'autre moitie, elle, ne tolere rien.
|
||||||
|
|
||||||
|
### Ce qui aura toujours besoin d'Internet, et c'est normal
|
||||||
|
|
||||||
|
Les correctifs de securite, la construction d'un ecosysteme neuf au-dela de ce que le cache
|
||||||
|
detient, la resolution des noms externes, le courriel sortant. Aucun n'est une operation
|
||||||
|
interne.
|
||||||
|
|
||||||
## 2026-09-11 (3) — `make expositions-etat` : le certificat SERVI, pas celui du disque
|
## 2026-09-11 (3) — `make expositions-etat` : le certificat SERVI, pas celui du disque
|
||||||
|
|
||||||
Renommer une exposition touche cinq choses. Quatre suivent au deploiement ; la cinquieme,
|
Renommer une exposition touche cinq choses. Quatre suivent au deploiement ; la cinquieme,
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 60 scripts expliques et atteignables, 117 cibles make documentees, 68 roles avec README. |
|
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 60 scripts expliques et atteignables, 117 cibles make documentees, 68 roles avec README. |
|
||||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (137 cle(s) declaree(s) par l'instance). |
|
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (138 cle(s) declaree(s) par l'instance). |
|
||||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 34 revendication(s) de port, aucune collision entre roles co-localises (36 groupes). |
|
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 34 revendication(s) de port, aucune collision entre roles co-localises (36 groupes). |
|
||||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 44 document(s) declarent leur lecteur (37 genere(s) exempte(s)). |
|
| P34 | Chaque document declare son lecteur | — | ✅ OK | 44 document(s) declarent leur lecteur (37 genere(s) exempte(s)). |
|
||||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||||
|
|
@ -55,14 +55,14 @@
|
||||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
| 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 : 56 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 56 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 |
|
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
|
||||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 155 regle(s) du site. |
|
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 151 regle(s) du site. |
|
||||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
||||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 89 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 89 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
||||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (125 lignes). |
|
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (125 lignes). |
|
||||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 217 regles `pass`), tous non consignes et tous motives. |
|
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 219 regles `pass`), tous non consignes et tous motives. |
|
||||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
||||||
|
|
@ -72,11 +72,11 @@
|
||||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (68 preuves, 68 roles, 41 groupes). |
|
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (68 preuves, 68 roles, 41 groupes). |
|
||||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `5f5af70` (publie le 2026-09-11). |
|
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `989398f` (publie le 2026-09-11). |
|
||||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
||||||
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). |
|
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). |
|
||||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
||||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 25 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ |
|
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 26 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ |
|
||||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
||||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
|
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
|
||||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 6 service(s) expose(s) portent le nom du plan (6 groupe(s) derive(s)). |
|
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 6 service(s) expose(s) portent le nom du plan (6 groupe(s) derive(s)). |
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.
|
# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.
|
||||||
remote: ssh://git@forge.genese.internal/genome/set-ops-public.wiki.git
|
remote: ssh://git@forge.genese.internal/genome/set-ops-public.wiki.git
|
||||||
source: 5f5af70
|
source: 989398f
|
||||||
date: 2026-09-11
|
date: 2026-09-11
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
| `serveur_debian` | ingress | echo-request | icmp | serveur_icinga | n-a | La supervision verifie que ce noeud repond (hostalive). Sans lui, elle le tient pour mort et supprime ses notifications. |
|
| `serveur_debian` | ingress | echo-request | icmp | serveur_icinga | n-a | La supervision verifie que ce noeud repond (hostalive). Sans lui, elle le tient pour mort et supprime ses notifications. |
|
||||||
| `serveur_debian` | ingress | frag-needed | icmp | externe | n-a | ICMP « fragmentation nécessaire » entrant : sans lui, un distant ne peut pas nous demander de réduire nos paquets — les transferts se figent. |
|
| `serveur_debian` | ingress | frag-needed | icmp | externe | n-a | ICMP « fragmentation nécessaire » entrant : sans lui, un distant ne peut pas nous demander de réduire nos paquets — les transferts se figent. |
|
||||||
| `serveur_debian` | egress | 80 | tcp | externe | clair | Dépôts apt en clair et redirections HTTP des miroirs (l'intégrité vient de la signature des paquets, pas du transport). |
|
| `serveur_debian` | egress | 80 | tcp | externe | clair | Dépôts apt en clair et redirections HTTP des miroirs (l'intégrité vient de la signature des paquets, pas du transport). |
|
||||||
| `serveur_debian` | egress | 123 | udp | externe | n-a | Synchronisation d'horloge (NTP). Une dérive fait échouer la validation des certificats step-ca et l'authentification SSO. |
|
| `serveur_debian` | egress | 123 | udp | frontiere | n-a | Synchronisation d'horloge (NTP) contre la frontière, autorité de temps de l'écosystème. Une dérive fait échouer la validation des certificats step-ca et l'authentification SSO. |
|
||||||
| `serveur_debian` | egress | 443 | tcp | externe | tls-requis | Dépôts apt en HTTPS (Debian, Smallstep, Grafana, Icinga, Forgejo, Nextcloud) — sans quoi aucun correctif de sécurité n'entre. |
|
| `serveur_debian` | egress | 443 | tcp | externe | tls-requis | Dépôts apt en HTTPS (Debian, Smallstep, Grafana, Icinga, Forgejo, Nextcloud) — sans quoi aucun correctif de sécurité n'entre. |
|
||||||
| `serveur_debian` | egress | frag-needed | icmp | externe | n-a | ICMP « fragmentation nécessaire » sortant : c'est ainsi que nos hôtes signalent l'overlay à 1450 aux correspondants distants. |
|
| `serveur_debian` | egress | frag-needed | icmp | externe | n-a | ICMP « fragmentation nécessaire » sortant : c'est ainsi que nos hôtes signalent l'overlay à 1450 aux correspondants distants. |
|
||||||
| `serveur_dovecot` | ingress | 24 | tcp | serveur_postfix | tls-requis | Remise LMTP depuis Postfix (edge-mta -> mailstore), en TLS vérifié (lmtp_tls_security_level=verify). |
|
| `serveur_dovecot` | ingress | 24 | tcp | serveur_postfix | tls-requis | Remise LMTP depuis Postfix (edge-mta -> mailstore), en TLS vérifié (lmtp_tls_security_level=verify). |
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,33 @@
|
||||||
# (source unique : inventories/<env>/group_vars/all/, panneau « Intrants » du GUI).
|
# (source unique : inventories/<env>/group_vars/all/, panneau « Intrants » du GUI).
|
||||||
# Laisser vide pour ne pas toucher au fuseau de la VM.
|
# Laisser vide pour ne pas toucher au fuseau de la VM.
|
||||||
chrony_timezone: "{{ fuseau_horaire | default('') }}"
|
chrony_timezone: "{{ fuseau_horaire | default('') }}"
|
||||||
|
|
||||||
|
# L'HEURE VIENT DE LA FRONTIERE (2026-09-11).
|
||||||
|
#
|
||||||
|
# Ce role posait le fuseau et INSTALLAIT chrony sans jamais toucher a ses sources : le
|
||||||
|
# defaut de Debian — `pool 2.debian.pool.ntp.org` — restait en place. Herite, jamais
|
||||||
|
# choisi. Mesure sur les quatorze machines de Chezlepro : quatre pairs publics chacune.
|
||||||
|
#
|
||||||
|
# C'etait la DERNIERE dependance vivante a Internet des operations internes. Tout le
|
||||||
|
# reste passe deja par le site : les paquets par son cache, les noms par son DNS, le
|
||||||
|
# genome par sa forge, l'etat par son depot.
|
||||||
|
#
|
||||||
|
# LA VALEUR EST DERIVEE, PAS ECRITE : `instancier` (tenant) et `site_inventaire` (site)
|
||||||
|
# la posent depuis l'underlay, parce qu'eux seuls savent par quelle patte leur ecosysteme
|
||||||
|
# atteint la frontiere. Un tenant sort par le lien de transit (ses passerelles de zone
|
||||||
|
# sont tenues par le SDN) ; une machine du site a la frontiere pour passerelle directe.
|
||||||
|
#
|
||||||
|
# VIDE = ON NE TOUCHE A RIEN. Un ecosysteme sans underlay monte n'a pas de frontiere a
|
||||||
|
# viser ; lui retirer ses sources sans rien mettre en face le laisserait sans horloge.
|
||||||
|
chrony_serveurs: []
|
||||||
|
chrony_conf: "/etc/chrony/chrony.conf"
|
||||||
|
chrony_conf_setops: "/etc/chrony/conf.d/setops.conf"
|
||||||
|
|
||||||
|
# SONDE « horloge » — ECART MAXIMAL TOLERE, EN MILLISECONDES.
|
||||||
|
#
|
||||||
|
# CHOIX D'EXPLOITATION, pas derivation. 500 ms est tres large pour un reseau local — la
|
||||||
|
# frontiere rendait -0,42 ms a la mesure — et c'est voulu : la sonde doit crier sur une
|
||||||
|
# horloge qui DECROCHE, pas sur la respiration normale d'un `chronyd`. Le seuil qui compte
|
||||||
|
# vraiment est ailleurs, dans l'autre moitie : la source retenue doit etre celle qu'on a
|
||||||
|
# declaree, et cette moitie-la ne tolere rien.
|
||||||
|
chrony_sonde_ecart_max: 500
|
||||||
|
|
|
||||||
5
roles/chrony/handlers/main.yml
Normal file
5
roles/chrony/handlers/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
- name: Restart chrony
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: chrony
|
||||||
|
state: restarted
|
||||||
|
|
@ -14,3 +14,35 @@
|
||||||
name: chrony
|
name: chrony
|
||||||
enabled: true
|
enabled: true
|
||||||
state: started
|
state: started
|
||||||
|
|
||||||
|
# LE DROP-IN AJOUTE, IL NE RETIRE PAS. `/etc/chrony/conf.d/` est lu EN PLUS du fichier
|
||||||
|
# principal : y declarer notre serveur sans neutraliser le `pool` de Debian laisserait la
|
||||||
|
# machine avec cinq sources, dont quatre sur Internet. On aurait alors centralise
|
||||||
|
# l'horloge en apparence et change strictement rien a la dependance.
|
||||||
|
- name: Retirer le pool public du fichier principal
|
||||||
|
ansible.builtin.replace:
|
||||||
|
path: "{{ chrony_conf }}"
|
||||||
|
regexp: '^(\s*(?:pool|server)\s+\S*(?:debian|ntp)\.(?:pool\.)?(?:ntp\.)?org.*)$'
|
||||||
|
replace: '# \1 # neutralise par Set-OPS : l horloge vient de la frontiere'
|
||||||
|
notify: Restart chrony
|
||||||
|
when: chrony_serveurs | length > 0
|
||||||
|
|
||||||
|
- name: Déclarer la frontière comme source de temps
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: setops.conf.j2
|
||||||
|
dest: "{{ chrony_conf_setops }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
notify: Restart chrony
|
||||||
|
when: chrony_serveurs | length > 0
|
||||||
|
|
||||||
|
# DEPOSEE ICI, DECLAREE PAR LE GROUPE `serveur_debian` — la derivation de supervision ne
|
||||||
|
# traverse pas les roles-taches. Meme lecon que `correctifs` et `audit`.
|
||||||
|
- name: Deposer la sonde d'horloge
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: sonde-horloge.sh.j2
|
||||||
|
dest: /usr/local/lib/setops/sondes/horloge.sh
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0750"
|
||||||
|
|
|
||||||
8
roles/chrony/templates/setops.conf.j2
Normal file
8
roles/chrony/templates/setops.conf.j2
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Gere par Set-OPS (role chrony). Ne pas editer a la main.
|
||||||
|
#
|
||||||
|
# L'autorite de temps de l'ecosysteme est la FRONTIERE. Elle se discipline sur le pool
|
||||||
|
# public quand Internet est la ; la flotte, elle, ne sort plus. Meme patron que le cache
|
||||||
|
# apt : un seul sort, les autres non.
|
||||||
|
{% for s in chrony_serveurs %}
|
||||||
|
server {{ s }} iburst
|
||||||
|
{% endfor %}
|
||||||
71
roles/chrony/templates/sonde-horloge.sh.j2
Normal file
71
roles/chrony/templates/sonde-horloge.sh.j2
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# GENERE par Set-OPS (role chrony). Ne pas editer a la main.
|
||||||
|
#
|
||||||
|
# SONDE « horloge » — la machine est-elle synchronisee, ET contre LA SOURCE DECLAREE ?
|
||||||
|
#
|
||||||
|
# LES DEUX MOITIES COMPTENT. Une machine peut etre parfaitement a l'heure en interrogeant
|
||||||
|
# quatre serveurs publics : c'est le defaut de Debian, et il a tenu quatorze machines
|
||||||
|
# jusqu'au 2026-09-11 sans que rien ne le dise. Mesurer la seule derive laisserait ce
|
||||||
|
# retour en arriere invisible.
|
||||||
|
#
|
||||||
|
# POURQUOI L'HEURE MERITE UNE SONDE. step-ca emet des certificats a duree courte ; TLS
|
||||||
|
# refuse un certificat pas encore valide ; Loki rejette une ligne trop loin dans le futur ;
|
||||||
|
# les silences d'Icinga reposent sur des `ttl`. Une flotte dont les horloges divergent
|
||||||
|
# tombe en panne de l'interieur, avec des symptomes qui ressemblent a tout sauf a une
|
||||||
|
# horloge.
|
||||||
|
#
|
||||||
|
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
|
||||||
|
# Mise en defaut PAR PARAMETRE : `chrony_sonde_ecart_max` (le baisser a 0).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ECART_MAX={{ chrony_sonde_ecart_max }}
|
||||||
|
ATTENDUS="{{ chrony_serveurs | join(' ') }}"
|
||||||
|
|
||||||
|
if ! systemctl is-active --quiet chrony; then
|
||||||
|
echo "chrony ne tourne pas : l'horloge derive sans que personne ne la corrige." \
|
||||||
|
"| ecart=U sources=0 declarees=0"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `^*` marque la source RETENUE. Sans elle, chrony tourne mais ne suit personne.
|
||||||
|
retenue=$(chronyc -n sources 2>/dev/null | awk '/^\^\*/ {print $2; exit}')
|
||||||
|
sources=$(chronyc -n sources 2>/dev/null | tail -n +3 | wc -l)
|
||||||
|
|
||||||
|
if [[ -z "${retenue}" ]]; then
|
||||||
|
echo "chrony tourne mais n'a retenu AUCUNE source (${sources} candidate(s)) :" \
|
||||||
|
"l'heure n'est plus disciplinee." \
|
||||||
|
"| ecart=U sources=${sources} declarees=0"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# L'ecart en millisecondes, en valeur absolue.
|
||||||
|
ecart=$(chronyc tracking 2>/dev/null \
|
||||||
|
| awk '/^Last offset/ {o=$4+0; if (o<0) o=-o; printf "%.0f", o*1000}')
|
||||||
|
# `$4+0` FORCE LE NUMERIQUE. `chronyc` prefixe la valeur d'un `+` ou d'un `-` ; sans la
|
||||||
|
# conversion, awk compare une CHAINE a zero et la valeur absolue ne mord pas. Le symptome
|
||||||
|
# etait discret — `ecart=-0ms` en perfdata — et c'est precisement ce qu'un grapheur avale
|
||||||
|
# sans rien dire.
|
||||||
|
ecart=${ecart:-0}
|
||||||
|
|
||||||
|
declarees=0
|
||||||
|
for s in ${ATTENDUS}; do
|
||||||
|
[[ "${retenue}" == "${s}" ]] && declarees=1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "${ATTENDUS}" && "${declarees}" -eq 0 ]]; then
|
||||||
|
echo "L'heure vient de ${retenue}, qui n'est PAS la source declaree (${ATTENDUS}) :" \
|
||||||
|
"la machine est sortie de l'ecosysteme pour se synchroniser." \
|
||||||
|
"| ecart=${ecart}ms;${ECART_MAX} sources=${sources} declarees=0"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( ecart > ECART_MAX )); then
|
||||||
|
echo "Synchronisee sur ${retenue}, mais l'ecart atteint ${ecart} ms" \
|
||||||
|
"(seuil ${ECART_MAX} ms) : les certificats a duree courte vont souffrir." \
|
||||||
|
"| ecart=${ecart}ms;${ECART_MAX} sources=${sources} declarees=1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Horloge disciplinee par ${retenue}, ecart ${ecart} ms." \
|
||||||
|
"| ecart=${ecart}ms;${ECART_MAX} sources=${sources} declarees=1"
|
||||||
|
exit 0
|
||||||
|
|
@ -55,9 +55,35 @@ flux:
|
||||||
partage: true
|
partage: true
|
||||||
raison: "La supervision verifie que ce noeud repond (hostalive). Sans lui, elle le tient pour mort et supprime ses notifications."
|
raison: "La supervision verifie que ce noeud repond (hostalive). Sans lui, elle le tient pour mort et supprime ses notifications."
|
||||||
|
|
||||||
|
# L'HEURE VIENT DE LA FRONTIÈRE, PAS D'INTERNET (2026-09-11).
|
||||||
|
#
|
||||||
|
# C'était `pair: externe` : les quatorze machines interrogeaient `2.debian.pool.ntp.org`,
|
||||||
|
# défaut de Debian jamais choisi — le rôle `chrony` posait le fuseau et laissait les
|
||||||
|
# sources telles quelles. Mesuré : quatre pairs publics par machine, sur les quatorze.
|
||||||
|
#
|
||||||
|
# C'était la DERNIÈRE dépendance vivante à Internet des opérations internes. Tout le
|
||||||
|
# reste sort déjà par le site : les paquets par son cache, les noms par son DNS, le
|
||||||
|
# génome par sa forge, l'état par son dépôt. L'heure, non.
|
||||||
|
#
|
||||||
|
# ET C'EST LA MAUVAISE À LAISSER TRAÎNER. step-ca émet des certificats à durée courte ;
|
||||||
|
# TLS refuse un certificat pas encore valide ; Loki rejette une ligne trop loin dans le
|
||||||
|
# futur ; les silences d'Icinga reposent sur des `ttl`. Une flotte dont les horloges
|
||||||
|
# divergent tombe en panne de l'intérieur, avec des symptômes qui ressemblent à tout
|
||||||
|
# sauf à une horloge. Et la dérive est lente : couper Internet ne casse rien le premier
|
||||||
|
# jour, ce qui laisse croire à une souveraineté qu'on n'a pas.
|
||||||
|
#
|
||||||
|
# La frontière est déjà `stratum 3`, synchronisée, et écoute en 123 sur chaque patte.
|
||||||
|
# Elle se discipline sur le pool public quand Internet est là ; la flotte, elle, ne sort
|
||||||
|
# plus. Même patron que le cache apt : un seul sort, les autres non.
|
||||||
|
#
|
||||||
|
# RESSERRE AUTANT QUE CENTRALISE : `pair: externe` autorisait le 123 vers N'IMPORTE
|
||||||
|
# QUELLE adresse d'Internet. La destination est désormais nommée.
|
||||||
- sens: egress
|
- sens: egress
|
||||||
port: 123
|
port: 123
|
||||||
protocole: udp
|
protocole: udp
|
||||||
pair: externe
|
pair: frontiere
|
||||||
chiffrement: n-a
|
chiffrement: n-a
|
||||||
raison: "Synchronisation d'horloge (NTP). Une dérive fait échouer la validation des certificats step-ca et l'authentification SSO."
|
raison: >-
|
||||||
|
Synchronisation d'horloge (NTP) contre la frontière, autorité de temps de
|
||||||
|
l'écosystème. Une dérive fait échouer la validation des certificats step-ca
|
||||||
|
et l'authentification SSO.
|
||||||
|
|
|
||||||
|
|
@ -32,3 +32,10 @@ sondes:
|
||||||
Des correctifs de securite attendent-ils d'etre appliques, et depuis quand ?
|
Des correctifs de securite attendent-ils d'etre appliques, et depuis quand ?
|
||||||
Set-OPS desarme les mises a jour automatiques et les applique au deploiement :
|
Set-OPS desarme les mises a jour automatiques et les applique au deploiement :
|
||||||
sans cette mesure, rien ne dit quand le geste est du.
|
sans cette mesure, rien ne dit quand le geste est du.
|
||||||
|
- nom: horloge
|
||||||
|
ttl: 5400
|
||||||
|
raison: >-
|
||||||
|
L'heure est-elle disciplinee, et par la SOURCE DECLAREE ? Une machine peut etre
|
||||||
|
parfaitement a l'heure en interrogeant quatre serveurs publics — c'est le defaut
|
||||||
|
de Debian, et il a tenu quatorze machines jusqu'au 2026-09-11 sans que rien ne le
|
||||||
|
dise. Mesurer la seule derive laisserait ce retour en arriere invisible.
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,8 @@ def flux_frontiere(flux_par_role: dict[str, list[dict]]) -> list[tuple[str, dict
|
||||||
(role, fl)
|
(role, fl)
|
||||||
for role, flux in sorted(flux_par_role.items())
|
for role, flux in sorted(flux_par_role.items())
|
||||||
for fl in flux
|
for fl in flux
|
||||||
if "externe" in _pairs(fl) or "voisins_site" in _pairs(fl)
|
if ("externe" in _pairs(fl) or "voisins_site" in _pairs(fl)
|
||||||
|
or "frontiere" in _pairs(fl))
|
||||||
]
|
]
|
||||||
retenus.sort(key=lambda rf: (rf[1].get("sens", ""), rf[0], str(rf[1].get("port"))))
|
retenus.sort(key=lambda rf: (rf[1].get("sens", ""), rf[0], str(rf[1].get("port"))))
|
||||||
return retenus
|
return retenus
|
||||||
|
|
@ -540,6 +541,12 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||||
}
|
}
|
||||||
destination = a
|
destination = a
|
||||||
entrant = fl.get("sens") == "ingress"
|
entrant = fl.get("sens") == "ingress"
|
||||||
|
if "frontiere" in _pairs(fl):
|
||||||
|
# TRAITE PAR LE BLOC DEDIE, PLUS BAS — et il faut sauter ICI, sinon le
|
||||||
|
# meme flux sort DEUX FOIS : une regle nommee vers `SETOPS_FRONTIERE`, et
|
||||||
|
# une seconde par le chemin generique vers `!SETOPS_INTERNES`. La seconde
|
||||||
|
# annulerait tout le benefice — elle rouvre le port vers l'Internet entier.
|
||||||
|
continue
|
||||||
# Le SSH de gestion est le SEUL flux `externe` a ne pas venir d'Internet :
|
# Le SSH de gestion est le SEUL flux `externe` a ne pas venir d'Internet :
|
||||||
# sa `raison` dit « l'acces depuis l'exterieur est filtre a l'OPNsense ».
|
# sa `raison` dit « l'acces depuis l'exterieur est filtre a l'OPNsense ».
|
||||||
ssh_gestion = entrant and "22" in _ports(fl)
|
ssh_gestion = entrant and "22" in _ports(fl)
|
||||||
|
|
@ -763,6 +770,8 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||||
for _fl in flux_par_role.get(_role, []):
|
for _fl in flux_par_role.get(_role, []):
|
||||||
_entrant = _fl.get("sens") == "ingress"
|
_entrant = _fl.get("sens") == "ingress"
|
||||||
_pairs_fl = _pairs(_fl)
|
_pairs_fl = _pairs(_fl)
|
||||||
|
if "frontiere" in _pairs_fl:
|
||||||
|
continue # bloc dedie, plus bas (voir la boucle des tenants)
|
||||||
if _entrant and "voisins_site" in _pairs_fl:
|
if _entrant and "voisins_site" in _pairs_fl:
|
||||||
# Un tenant qui s'adresse au site : le paquet penetre le pare-feu par
|
# Un tenant qui s'adresse au site : le paquet penetre le pare-feu par
|
||||||
# le lien de TRANSIT, pas par la patte du site. Une regle par voisin,
|
# le lien de TRANSIT, pas par la patte du site. Une regle par voisin,
|
||||||
|
|
@ -1103,6 +1112,56 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||||
_uniques.append(_r)
|
_uniques.append(_r)
|
||||||
regles = _uniques
|
regles = _uniques
|
||||||
|
|
||||||
|
# --- LA FRONTIERE COMME PAIR : L'HEURE (2026-09-11) ----------------------------
|
||||||
|
#
|
||||||
|
# `pair: frontiere` designe la frontiere ELLE-MEME comme destination — ni l'Internet,
|
||||||
|
# ni un voisin, ni un role du site. Le seul usage aujourd'hui est l'horloge : le socle
|
||||||
|
# declare `egress 123/udp` vers elle, et elle repond en `stratum 3`.
|
||||||
|
#
|
||||||
|
# POURQUOI UN BLOC A PART plutot qu'une branche de plus dans les deux boucles
|
||||||
|
# ci-dessus : la regle a exactement la MEME forme pour un locataire et pour une
|
||||||
|
# machine du site — seule change la patte d'arrivee. L'enfiler dans deux conditionnels
|
||||||
|
# deja longs aurait duplique la meme intention a deux endroits, et c'est ainsi qu'une
|
||||||
|
# moitie finit par prendre du retard sur l'autre.
|
||||||
|
#
|
||||||
|
# RIEN N'EST ECRIT EN DUR : le port, le protocole et la raison viennent de la
|
||||||
|
# declaration. Le jour ou un second flux vise la frontiere, il sort d'ici sans qu'on
|
||||||
|
# y touche.
|
||||||
|
_flux_frontiere = [(r, f) for r, flux in sorted(flux_par_role.items()) for f in flux
|
||||||
|
if "frontiere" in _pairs(f) and f.get("sens") != "ingress"]
|
||||||
|
if _flux_frontiere:
|
||||||
|
_pattes_frontiere = sorted({str(h["ip"]) for h in underlay_mod.hotes(_u)
|
||||||
|
if h.get("role") == "frontiere" and h.get("ip")
|
||||||
|
and str(h.get("etat", "actif")) != "reserve"})
|
||||||
|
if _pattes_frontiere:
|
||||||
|
alias["SETOPS_FRONTIERE"] = {
|
||||||
|
"type": "host",
|
||||||
|
"contenu": _pattes_frontiere,
|
||||||
|
"description": "Pattes de la frontiere active (autorite de temps)",
|
||||||
|
}
|
||||||
|
# LES PATTES D'ARRIVEE. Un locataire entre par le lien de transit — ses
|
||||||
|
# passerelles de zone sont tenues par le SDN, pas par la frontiere. Une
|
||||||
|
# machine du site entre par la patte de SA zone. D-61 : on raisonne en
|
||||||
|
# ARRIVEE, donc `in` dans les deux cas.
|
||||||
|
_portees_f = [(f"SETOPS_TENANT_{p2}{n2['index']}", if_transit)
|
||||||
|
for _n2, p2, n2 in tenants]
|
||||||
|
_portees_f += [("SETOPS_SITE", z) for z in
|
||||||
|
sorted({_if_de(m.get("reseau")) for m in _machines_site})]
|
||||||
|
for _role_f, _fl_f in _flux_frontiere:
|
||||||
|
for _src_f, _if_f in _portees_f:
|
||||||
|
regles.append({
|
||||||
|
"sens": "in",
|
||||||
|
"interface": _if_f,
|
||||||
|
"protocole": _fl_f.get("protocole", "udp"),
|
||||||
|
"source": _src_f,
|
||||||
|
"destination": "SETOPS_FRONTIERE",
|
||||||
|
"ports": _ports(_fl_f),
|
||||||
|
"chiffrement": _fl_f.get("chiffrement"),
|
||||||
|
"role": _role_f,
|
||||||
|
"tenant": "",
|
||||||
|
"raison": _fl_f.get("raison", ""),
|
||||||
|
})
|
||||||
|
|
||||||
# Adresse du noeud de sortie primaire sur le lien de frontiere, calculee une fois :
|
# Adresse du noeud de sortie primaire sur le lien de frontiere, calculee une fois :
|
||||||
# elle sert au prochain saut des routes ET au commentaire qui l'explique.
|
# elle sert au prochain saut des routes ET au commentaire qui l'explique.
|
||||||
_ip_sortie = adresse_noeud_de_sortie(underlay_mod.charger(), transit)
|
_ip_sortie = adresse_noeud_de_sortie(underlay_mod.charger(), transit)
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,24 @@ def generer() -> dict:
|
||||||
if groupe and len(exp) == 1:
|
if groupe and len(exp) == 1:
|
||||||
hostname_par_groupe[groupe] = exp[0]
|
hostname_par_groupe[groupe] = exp[0]
|
||||||
|
|
||||||
|
# La patte de la frontiere que les tenants traversent — calculee une fois.
|
||||||
|
_temps_frontiere: list = []
|
||||||
|
try:
|
||||||
|
_carte = underlay_mod.charger()
|
||||||
|
if _carte:
|
||||||
|
# LE LIEN DE TRANSIT SE DECLARE, IL NE SE DEVINE PAS. `reseau_transit()`
|
||||||
|
# le derive de `passerelle_sortie` — le champ qui dit « c'est par la qu'on
|
||||||
|
# sort ». Le reconnaitre a son prefixe d'adresse aurait marche ici et menti
|
||||||
|
# chez le prochain hebergeur.
|
||||||
|
_t = underlay_mod.reseau_transit(_carte) or {}
|
||||||
|
_res_transit = {str(_t.get("nom"))} if _t.get("nom") else set()
|
||||||
|
_temps_frontiere = sorted({str(h["ip"]) for h in underlay_mod.hotes(_carte)
|
||||||
|
if h.get("role") == "frontiere" and h.get("ip")
|
||||||
|
and str(h.get("reseau")) in _res_transit
|
||||||
|
and str(h.get("etat", "actif")) != "reserve"})
|
||||||
|
except Exception:
|
||||||
|
_temps_frontiere = []
|
||||||
|
|
||||||
services_par_hote: dict = {}
|
services_par_hote: dict = {}
|
||||||
for app in apps.values():
|
for app in apps.values():
|
||||||
services_par_hote.setdefault(app.get("hote"), set()).add(app.get("groupe"))
|
services_par_hote.setdefault(app.get("hote"), set()).add(app.get("groupe"))
|
||||||
|
|
@ -330,6 +348,17 @@ def generer() -> dict:
|
||||||
sans = sorted({e["fqdn"] for e in expositions if e.get("edge") in groupes})
|
sans = sorted({e["fqdn"] for e in expositions if e.get("edge") in groupes})
|
||||||
if sans:
|
if sans:
|
||||||
hostvars["sans_exposition"] = sans
|
hostvars["sans_exposition"] = sans
|
||||||
|
# L'AUTORITE DE TEMPS, DERIVEE DE L'UNDERLAY (2026-09-11).
|
||||||
|
#
|
||||||
|
# Un tenant atteint la frontiere par le LIEN DE TRANSIT : ses passerelles de zone
|
||||||
|
# (10.17.x.1) sont tenues par le SDN de Proxmox, pas par elle. On prend donc la
|
||||||
|
# patte de la frontiere active posee sur le reseau que les tenants traversent.
|
||||||
|
#
|
||||||
|
# `etat: reserve` est exclu : la seconde frontiere est declaree et ne porte encore
|
||||||
|
# aucune adresse. Vide si aucun underlay — le role ne touchera alors a rien.
|
||||||
|
if _temps_frontiere:
|
||||||
|
hostvars.setdefault("chrony_serveurs", _temps_frontiere)
|
||||||
|
|
||||||
# Nom public de chaque service porte par cet hote, tel que le plan l'expose.
|
# Nom public de chaque service porte par cet hote, tel que le plan l'expose.
|
||||||
for groupe_expose in sorted(groupes & set(hostname_par_groupe)):
|
for groupe_expose in sorted(groupes & set(hostname_par_groupe)):
|
||||||
hostvars.setdefault(f"{groupe_expose}_hostname",
|
hostvars.setdefault(f"{groupe_expose}_hostname",
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ CHIFFREMENT = {"tls-requis", "tls", "starttls", "ssh", "tls-cible", "clair", "n-
|
||||||
# tenants d'a cote) : c'est le SOCLE sur lequel les uns et les autres reposent. Le seul
|
# tenants d'a cote) : c'est le SOCLE sur lequel les uns et les autres reposent. Le seul
|
||||||
# a s'y adresser est le runner du site, et c'est tout son objet — materialiser.
|
# a s'y adresser est le runner du site, et c'est tout son objet — materialiser.
|
||||||
MOTS_PAIR = {"edge", "flotte", "externe", "localhost", "expositions", "derive", "admin",
|
MOTS_PAIR = {"edge", "flotte", "externe", "localhost", "expositions", "derive", "admin",
|
||||||
"voisins_site", "fabric", "runner_site"}
|
"voisins_site", "fabric", "frontiere", "runner_site"}
|
||||||
GROUPE_EDGE = "serveur_nginx"
|
GROUPE_EDGE = "serveur_nginx"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -245,6 +245,31 @@ def _adresses_fabric() -> list[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _adresses_frontiere() -> list[str]:
|
||||||
|
"""Les pattes de la frontiere ACTIVE — l'autorite de temps de l'ecosysteme.
|
||||||
|
|
||||||
|
PLUS ETROIT QUE `fabric`, ET C'EST LE POINT. `_adresses_fabric()` rend les
|
||||||
|
hyperviseurs ET la frontiere : s'en servir pour l'heure ouvrirait le 123 vers des
|
||||||
|
machines qui ne le servent pas. Une paire dit CE QU'EST le pair, pas ou il se trouve.
|
||||||
|
|
||||||
|
`etat: reserve` est exclu : `bifrost-2` est declaree et ne porte aucune adresse. La
|
||||||
|
compter rendrait une regle vers une machine qui n'existe pas encore — et le jour ou
|
||||||
|
elle existera, elle heritera de la regle sans que personne ne l'ait decide.
|
||||||
|
|
||||||
|
Rend [] quand aucun underlay n'est monte : degrader, jamais deviner.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import underlay as _u
|
||||||
|
carte = _u.charger()
|
||||||
|
if not carte:
|
||||||
|
return []
|
||||||
|
return sorted({str(h["ip"]) for h in _u.hotes(carte)
|
||||||
|
if h.get("role") == "frontiere" and h.get("ip")
|
||||||
|
and str(h.get("etat", "actif")) != "reserve"})
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict[str, str],
|
def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict[str, str],
|
||||||
site: bool = False) -> list[str]:
|
site: bool = False) -> list[str]:
|
||||||
"""pair -> IP sources concretes (pour une regle ingress). [] = pas de regle inter-noeud."""
|
"""pair -> IP sources concretes (pour une regle ingress). [] = pas de regle inter-noeud."""
|
||||||
|
|
@ -258,6 +283,13 @@ def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict
|
||||||
# `nftables_admin_ssh` ; `voisins_site` des supernets des autres tenants
|
# `nftables_admin_ssh` ; `voisins_site` des supernets des autres tenants
|
||||||
# federes ; `runner_site` du plan du SITE. Traites a part, plus bas.
|
# federes ; `runner_site` du plan du SITE. Traites a part, plus bas.
|
||||||
continue
|
continue
|
||||||
|
if p == "frontiere":
|
||||||
|
# LA FRONTIERE EST UN PAIR A ELLE SEULE (2026-09-11). Comme `fabric`, elle
|
||||||
|
# vit dans la carte de l'hebergeur et non dans un groupe d'hotes — mais plus
|
||||||
|
# etroite : `fabric` couvre aussi les hyperviseurs, qui ne servent pas
|
||||||
|
# l'heure. Resolue plus bas, jamais laissee tomber dans le cas general : une
|
||||||
|
# source vide produirait une regle sans `saddr`.
|
||||||
|
continue
|
||||||
if p == "fabric":
|
if p == "fabric":
|
||||||
# LA FABRIC N'EST PAS UN GROUPE D'HOTES, ELLE EST DANS LA CARTE (2026-09-10).
|
# LA FABRIC N'EST PAS UN GROUPE D'HOTES, ELLE EST DANS LA CARTE (2026-09-10).
|
||||||
#
|
#
|
||||||
|
|
@ -285,6 +317,8 @@ def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict
|
||||||
srcs |= set(_sources_admin_ssh_site() if site else _sources_admin_ssh())
|
srcs |= set(_sources_admin_ssh_site() if site else _sources_admin_ssh())
|
||||||
if "fabric" in demandes:
|
if "fabric" in demandes:
|
||||||
srcs |= set(_adresses_fabric())
|
srcs |= set(_adresses_fabric())
|
||||||
|
if "frontiere" in demandes:
|
||||||
|
srcs |= set(_adresses_frontiere())
|
||||||
if "voisins_site" in demandes:
|
if "voisins_site" in demandes:
|
||||||
# DU COTE DU SITE, « VOISIN » N'EXCLUT PERSONNE (mesure du 2026-09-02).
|
# DU COTE DU SITE, « VOISIN » N'EXCLUT PERSONNE (mesure du 2026-09-02).
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,16 @@ def inventaire() -> dict:
|
||||||
if str(a.get("hote")) == nom_hote and a.get("groupe")}
|
if str(a.get("hote")) == nom_hote and a.get("groupe")}
|
||||||
|
|
||||||
par_reseau = {r.get("nom"): r for r in U.reseaux(u)}
|
par_reseau = {r.get("nom"): r for r in U.reseaux(u)}
|
||||||
|
# L'AUTORITE DE TEMPS, COTE SITE (2026-09-11).
|
||||||
|
#
|
||||||
|
# Ici la frontiere est la passerelle DIRECTE de chaque zone — pas besoin de passer par
|
||||||
|
# le lien de transit comme un tenant. On prend donc la patte de la frontiere posee sur
|
||||||
|
# LA ZONE DE LA MACHINE : le chemin le plus court, et celui qu'aucune regle
|
||||||
|
# supplementaire n'a besoin d'ouvrir.
|
||||||
|
_temps_par_reseau = {str(_h.get("reseau")): str(_h.get("ip"))
|
||||||
|
for _h in U.hotes(u)
|
||||||
|
if _h.get("role") == "frontiere" and _h.get("ip")
|
||||||
|
and str(_h.get("etat", "actif")) != "reserve"}
|
||||||
# Les sous-reseaux ou vivent REELLEMENT des machines du site : l'etendue de
|
# Les sous-reseaux ou vivent REELLEMENT des machines du site : l'etendue de
|
||||||
# l'ecosysteme, telle que le plan la dessine.
|
# l'ecosysteme, telle que le plan la dessine.
|
||||||
_zones_du_site = sorted({str((par_reseau.get(s.get("reseau")) or {}).get("sous_reseau"))
|
_zones_du_site = sorted({str((par_reseau.get(s.get("reseau")) or {}).get("sous_reseau"))
|
||||||
|
|
@ -368,6 +378,9 @@ def inventaire() -> dict:
|
||||||
"site_pont": r.get("pont"),
|
"site_pont": r.get("pont"),
|
||||||
"site_noeud": srv.get("noeud"),
|
"site_noeud": srv.get("noeud"),
|
||||||
"proxmox_vmid": srv.get("vmid"),
|
"proxmox_vmid": srv.get("vmid"),
|
||||||
|
# La frontiere, autorite de temps — sa patte SUR LA ZONE DE CETTE MACHINE.
|
||||||
|
"chrony_serveurs": ([_temps_par_reseau[str(srv.get("reseau"))]]
|
||||||
|
if str(srv.get("reseau")) in _temps_par_reseau else []),
|
||||||
# L'espace d'adressage de cet ecosysteme. Un tenant le derive de son index ;
|
# L'espace d'adressage de cet ecosysteme. Un tenant le derive de son index ;
|
||||||
# le site le tient du reseau ou vivent ses machines. Meme sens, autre source.
|
# 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.
|
# `serveur_resolveur` s'en sert pour savoir QUI a le droit de l'interroger.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue