From 84b5ecc71d9c4741595baf65e23e79b998014a9e Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Thu, 10 Sep 2026 10:27:12 -0400 Subject: [PATCH] frontiere posee, et cinq sondes qui disaient faux Application de ce que l entree precedente avait prepare : 49 regles creees, 5 retirees. Cibles Prometheus 2/8 -> 8/8, hotes dans Loki 1/7 -> 7/7. Puis correction de ce que l application a revele. Cinq defauts, tous de la meme famille : un reglage qui ne suit pas l interrupteur dont il depend. - la sonde de Loki interrogeait en HTTPS un Loki servant en clair ; - la sonde de la forge visait http:// sur un port 443 chiffre, puis validait un certificat emis pour le FQDN en interrogeant 127.0.0.1 ; - la sonde du runner comparait six depots a UNE branche quand le plan en declare une par depot. Elle contredisait la declaration qu elle etait censee verifier : elle n accusait pas la machine, elle s accusait elle-meme. Le tenant restait juste dans les cinq cas. C est le site, qui deploie ces roles autrement, qui les a tous reveles d un coup. La sonde des journaux, elle, avait deux defauts a la naissance. Elle criait une perte deja reparee, faute de dire sur QUELLE fenetre - un ecart sans sa fenetre n est pas une mesure, c est un nombre. Et elle alertait sur la cicatrice plutot que sur la plaie : le compteur d Alloy est cumulatif, donc six machines restaient orange pour toujours. Ce qui alerte est desormais la CROISSANCE. En echange, un cas muet leve maintenant : zero envoi et zero rejet n est pas la sante, c est un agent qui ne fait rien. Enfin, le runner ne pouvait plus cloner le genome. Defaut PREEXISTANT, revele par le redeploiement et non cause par lui - le diff des regles generees le montre. generer_nftables(site=True) lisait le plan du TENANT : derive se resolvait donc a 3000, le port que Forgejo ecoute derriere un edge, alors que le plan du site dit 443 et explique pourquoi. La forge ouvrait un port que personne n ecoute et laissait 443 ferme a toute la flotte. Mesure finale : Icinga 51/51, Prometheus 8/8, Loki 7/7, prouver 64 OK, lint 0 defaut sur 82 fichiers. Controles negatifs sur des COPIES des sondes : les quatre rendent CRITICAL. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q --- CHANGELOG.md | 135 ++++++++++++++++++ .../templates/sonde-journaux.sh.j2 | 63 ++++++-- roles/serveur_forgejo/defaults/main.yml | 14 +- .../templates/sonde-forge.sh.j2 | 11 +- roles/serveur_loki/defaults/main.yml | 14 +- roles/serveur_ops/defaults/main.yml | 14 ++ .../serveur_ops/templates/sonde-runner.sh.j2 | 12 +- scripts/resoudre_flux.py | 33 ++++- 8 files changed, 279 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 252579f..d70aeee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,140 @@ # CHANGELOG — Set-OPS +## 2026-09-10 (6) — La frontiere passe, et trois sondes disaient faux + +Application de ce que l'entree precedente avait prepare, puis correction de ce que +l'application a revele. + +### La frontiere + +`make frontiere-appliquer` : **49 regles creees, 5 retirees.** Effet mesure immediatement : + + cibles Prometheus 2/8 -> 8/8 + hotes dans Loki 1/7 -> 7/7 + +### Trois defauts, tous de la meme famille : un reglage qui ne suit pas son interrupteur + +**1. La sonde de Loki interrogeait en HTTPS un Loki servant en clair.** +`serveur_loki_sonde_url` disait `https` en dur alors que `serveur_loki_tls_actif` vaut +`false` par defaut. Chez le tenant, qui l'active dans ses group_vars, l'accord etait +fortuit ; au site, qui ne l'active pas, la sonde rapportait *« Loki ne repond pas »* sur un +service en parfaite sante. **Une fausse alarme est pire qu'une sonde absente : elle apprend +a ne plus lire la sonde.** Le schema se derive desormais du commutateur. + +C'est exactement le meme defaut que `GF_AUTH_DISABLE_LOGIN_FORM` chez Grafana, corrige la +veille au soir. Deux occurrences en douze heures. + +**2. La sonde des journaux criait une perte deja reparee.** +Elle comparait au passage precedent sans dire QUAND ce passage avait eu lieu. Elle a +annonce *PERTE EN COURS* sur deux machines alors que le delta reel etait nul — les lignes +avaient ete perdues avant la reparation. Mesure de controle, deux lectures a 90 s : + + jetees 11340 -> 11340 (delta 0) + envoyees 68 -> 86 (delta +18) + +*Un ecart sans sa fenetre n'est pas une mesure, c'est un nombre.* L'etat retient +maintenant l'horodatage, et le message porte la duree. + +**3. Et surtout : elle alertait sur la cicatrice, pas sur la plaie.** +Le compteur d'Alloy est cumulatif — il ne redescend qu'au redemarrage. Les six machines +qui avaient perdu des lignes pendant que la frontiere etait fermee les portaient donc pour +toujours, condamnees a l'orange permanent. **Ce qui alerte est la CROISSANCE.** Le total +reste dans le texte et dans les metriques, la ou il sert au diagnostic sans crier. + +En echange, un cas qui ne levait rien le fait desormais : `envoyees == 0` et `jetees == 0` +n'est pas « tout va bien », c'est « Alloy ne fait rien du tout ». + +Controles negatifs, sur des COPIES des sondes : + + port d'Alloy inexistant -> CRITICAL rc=2 + compteur de pertes en hausse -> CRITICAL rc=2 + port de node_exporter ferme -> CRITICAL rc=2 + +### Neuf services qu'Icinga attendait et que personne ne lui envoyait + +Le temoin declarait 51 services et n'en recevait que 42. Les neuf muets avaient une sortie +**vide** : jamais un seul resultat. `setops-sondes.conf` derive les services de tous les +`meta/supervision.yml` — Icinga savait donc les attendre bien avant que les roles n'aient +ete rejoues pour deposer les scripts. + +*Une declaration suffit a creer l'attente ; il faut un deploiement pour creer la reponse.* +Huit roles rejoues au site : `serveur_artefacts`, `serveur_resolveur`, `serveur_powerdns`, +`serveur_forgejo`, `serveur_postgresql`, `serveur_postfix`, `serveur_step_ca`, `serveur_ops`. + +### Grafana + +`vault_grafana_admin` depose dans la voute du site. Verifie sur la machine : formulaire +local **offert**, zero ligne `GENERIC_OAUTH`. Sonde `tableaux` verte. + +*Note d'exploitation :* `ansible-vault` refuse de tourner quand stdout n'est pas bloquant — +il faut le faire passer par un tube. Un premier essai a tronque la voute de 11,8 K a 873 o +parce que le chiffrement s'est execute apres une lecture qui avait echoue. Restauree depuis +la copie prise avant, identique a l'octet pres. **Le remede est dans l'ordre des gardes : +lire, VERIFIER le nombre de cles, ecrire vers un fichier neuf, verifier ce fichier, et +seulement alors remplacer.** + +### Etat mesure + +| | | +|---|---| +| cibles Prometheus | **8/8** | +| hotes dans Loki | **7/7** | +| sonde `journaux` | **7/7 vert** | +| sonde `metriques` | **7/7 vert** | +| services Icinga | 51, dont 42 OK avant redeploiement des huit roles | +| `make prouver` | 64 OK, 0 echec | +| `ansible-lint` | 0 defaut, profil `production` | + +### Suite : le runner ne pouvait plus cloner, et trois sondes de plus disaient faux + +Redeployer `serveur_ops` au site a echoue sur le clonage du genome. Mesure depuis les sept +machines : + + 10.0.33.11:443 injoignable de PARTOUT sauf depuis la forge elle-meme + +Y compris depuis `site-cache-01`, qui est dans le MEME sous-reseau — ce qui excluait la +frontiere et designait le pare-feu de l'hote. Le diff de `flux-genere/` l'a confirme : la +ligne fautive etait la AVANT nos changements, qui n'ajoutaient que les regles des sondes. +**Defaut preexistant, revele par le redeploiement, pas cause par lui.** + +**La cause : `generer_nftables(site=True)` lisait le plan du TENANT.** `ports_plan = +_ports_du_plan()`, sans condition. `derive` se resolvait donc contre +`OPS-Chezlepro/plan/applications.yml`, ou Forgejo vaut 3000 — le port qu'il ecoute DERRIERE +un edge. Le plan du site dit 443, et le disait explicitement : + + # 443, ET NON 3000. [...] garder 3000 aurait grave `:3000` dans ROOT_URL + +La regle d'hote de la forge ouvrait donc un port que personne n'ecoute et laissait 443 +ferme a toute la flotte. Un seul registre interroge pour deux verites opposees. + +**Trois sondes de plus corrigees, toutes de la meme famille** — un parametre qui ne suit +pas l'interrupteur dont il depend : + +| sonde | ce qu'elle disait | la cause | +|---|---|---| +| `runner` | 2 depots divergent | comparait les 6 depots a UNE branche, quand le plan en declare une PAR depot (`master` pour deux d'entre eux) | +| `forge` | reponse illisible | `http://` en dur sur un port 443 servant du TLS | +| `forge` | ne repond pas | `curl -s` sans `-k` : cert step-ca emis pour le FQDN, interroge sur `127.0.0.1` | + +La sonde `runner` est l'exemple le plus net : **elle contredisait la declaration qu'elle +etait censee verifier.** Elle n'accusait pas la machine, elle s'accusait elle-meme. + +Avec Grafana, Loki et Forgejo, cela fait **cinq occurrences du meme defaut en une journee**. +Le motif merite d'etre nomme : *un reglage corrige a moitie ne dit rien tant que le +deploiement ne change pas de camp.* Le tenant restait juste dans les cinq cas — c'est le +site, qui deploie ces roles autrement, qui les a tous reveles d'un coup. + +### Etat final mesure + + Icinga 51 / 51 services OK + Prometheus 8 / 8 cibles up + Loki 7 / 7 hotes presents + prouver 64 OK, 0 echec + lint 0 defaut sur 82 fichiers, profil production + +Controles negatifs, sur des COPIES : port d'Alloy, compteur de pertes, node_exporter, +forge — les quatre rendent CRITICAL. + ## 2026-09-10 (5) — Le site prend sa propre pile d'observabilite Suite directe de D-87. La decision disait : *l'hebergeur n'a pas le droit de voir les diff --git a/roles/client_journal/templates/sonde-journaux.sh.j2 b/roles/client_journal/templates/sonde-journaux.sh.j2 index c051cf8..7e69d60 100644 --- a/roles/client_journal/templates/sonde-journaux.sh.j2 +++ b/roles/client_journal/templates/sonde-journaux.sh.j2 @@ -25,28 +25,69 @@ envoyees=$(printf '%s\n' "${corps}" \ | awk '/^loki_write_sent_entries_total\{/ {s += $2} END {printf "%d", s+0}') # LA MEMOIRE D'UN PASSAGE : sans elle, un compteur cumulatif ne dit pas QUAND. +# +# ET LE MOMENT DE LA LECTURE PRECEDENTE AVEC ELLE (2026-09-10). Sans horodatage, « depuis +# le dernier passage » est une fenetre de duree inconnue : la sonde a annonce une PERTE EN +# COURS sur deux machines alors que le delta reel etait nul — les lignes avaient ete +# perdues entre une lecture manuelle de la veille et la reparation intervenue depuis. +# +# Un ecart sans sa fenetre n'est pas une mesure, c'est un nombre. On garde donc les deux, +# et le message porte la duree. precedent=0 -[[ -r "${ETAT}" ]] && precedent=$(cat "${ETAT}" 2>/dev/null | tr -cd '0-9') -: "${precedent:=0}" -printf '%s' "${jetees}" > "${ETAT}" 2>/dev/null || true +precedent_a=0 +if [[ -r "${ETAT}" ]]; then + read -r precedent precedent_a < "${ETAT}" 2>/dev/null || true +fi +precedent=$(printf '%s' "${precedent:-0}" | tr -cd '0-9'); : "${precedent:=0}" +precedent_a=$(printf '%s' "${precedent_a:-0}" | tr -cd '0-9'); : "${precedent_a:=0}" +maintenant=$(date +%s) +printf '%s %s' "${jetees}" "${maintenant}" > "${ETAT}" 2>/dev/null || true perf="envoyees=${envoyees} jetees=${jetees}" nouvelles=$(( jetees - precedent )) +fenetre=$(( maintenant - precedent_a )) +# CE QUI ALERTE, C'EST LA CROISSANCE — PAS LA CICATRICE (2026-09-10). +# +# Le compteur d'Alloy est cumulatif : il ne redescend qu'au redemarrage du demon. Une +# machine qui a perdu onze mille lignes pendant que la frontiere etait fermee les portera +# donc pour toujours. Faire de ce total un AVERTISSEMENT, c'est condamner six machines sur +# sept a l'orange permanent — et une sonde toujours orange est une sonde qu'on cesse de +# lire, ce qui la rend pire qu'absente. +# +# La perte ANCIENNE n'est pas une cause d'action : elle est reparee, ou elle ne l'est pas, +# et c'est le delta qui le dit. Le total reste dans le texte et dans les metriques, la ou +# il sert au diagnostic sans crier. if (( nouvelles > 0 )); then - echo "PERTE EN COURS : ${nouvelles} ligne(s) de journal jetees depuis le dernier passage (${jetees} au total). | ${perf}" + echo "PERTE EN COURS : ${nouvelles} ligne(s) jetees en ${fenetre} s (${jetees} au total). | ${perf}" exit 2 fi -# UN COMPTEUR QUI RECULE veut dire qu'Alloy a redemarre — ses compteurs repartent de -# zero. Ce n'est pas une perte, c'est une remise a zero, et la dire pour ce qu'elle est -# evite de chercher une panne la ou il y a eu un redemarrage. -if (( nouvelles < 0 )); then - echo "Alloy a redemarre (compteurs remis a zero) ; ${jetees} ligne(s) jetees depuis. | ${perf}" + +# UN SILENCE COMPLET EST SUSPECT, LUI. Zero ligne expediee ET zero jetee ne veut pas dire +# « tout va bien » : ca veut dire qu'Alloy ne fait rien du tout — source mal ciblee, +# journal vide, composant non demarre. Ce cas-la merite d'etre vu. +if (( envoyees == 0 )); then + echo "Alloy n'a expedie AUCUNE ligne : ni envoi, ni rejet — rien ne remonte. | ${perf}" exit 1 fi + +# UN COMPTEUR QUI RECULE veut dire qu'Alloy a redemarre : ses compteurs repartent de zero. +# Ce n'est pas une perte, et le dire evite de chercher une panne la ou il y a eu un +# redemarrage. +if (( nouvelles < 0 )); then + echo "Alloy a redemarre ; ${envoyees} ligne(s) expediees depuis, aucune perte nouvelle. | ${perf}" + exit 0 +fi + +# UNE PREMIERE LECTURE N'A PAS DE FENETRE : rien a comparer. On rend l'etat, pas un verdict. +if (( precedent_a == 0 )); then + echo "Premiere lecture : ${envoyees} ligne(s) expediees, ${jetees} jetees cumulees. | ${perf}" + exit 0 +fi + if (( jetees > 0 )); then - echo "Journaux expedies ; ${jetees} ligne(s) perdues par le passe, plus rien depuis. | ${perf}" - exit 1 + echo "Journaux expedies ; aucune perte depuis ${fenetre} s (${jetees} jetees par le passe). | ${perf}" + exit 0 fi echo "Journaux expedies vers Loki, aucune ligne perdue. | ${perf}" exit 0 diff --git a/roles/serveur_forgejo/defaults/main.yml b/roles/serveur_forgejo/defaults/main.yml index 8378ce5..628f3f4 100644 --- a/roles/serveur_forgejo/defaults/main.yml +++ b/roles/serveur_forgejo/defaults/main.yml @@ -159,4 +159,16 @@ serveur_forgejo_signature_url: "{{ serveur_forgejo_url }}.asc" # `http_port` (l'edge fait le TLS devant). Viser 443 en dur, comme la premiere version de # la sonde, cassait sur toute forge derriere un edge — c'est-a-dire toutes celles des # tenants. -serveur_forgejo_sonde_url: "http://127.0.0.1:{{ serveur_forgejo_http_port }}" +# +# ET LE SCHEMA SUIT `serveur_forgejo_tls`, comme le port suit `http_port` (2026-09-10). +# La ligne corrigeait le port et laissait `http` en dur : au SITE, ou la forge sert son +# propre TLS sur 443, la sonde envoyait du clair a un port chiffre et recevait +# « Client sent an HTTP request to an HTTPS server » — un CRITICAL sur une forge qui +# servait parfaitement le genome au meme instant. +# +# Troisieme occurrence du meme defaut dans la meme journee (Grafana, Loki, ici) : un +# parametre corrige a moitie, dont la moitie restee en dur ne dit rien tant que le +# deploiement ne change pas de camp. +serveur_forgejo_sonde_schema: "{{ 'https' if serveur_forgejo_tls | bool else 'http' }}" +serveur_forgejo_sonde_url: >- + {{ serveur_forgejo_sonde_schema }}://127.0.0.1:{{ serveur_forgejo_http_port }} diff --git a/roles/serveur_forgejo/templates/sonde-forge.sh.j2 b/roles/serveur_forgejo/templates/sonde-forge.sh.j2 index 1637f7c..727aa07 100644 --- a/roles/serveur_forgejo/templates/sonde-forge.sh.j2 +++ b/roles/serveur_forgejo/templates/sonde-forge.sh.j2 @@ -20,7 +20,16 @@ set -uo pipefail URL={{ serveur_forgejo_sonde_url }} -sortie=$(curl -s --max-time 10 "${URL}/api/healthz" 2>/dev/null) +# `-k`, ET C'EST DELIBERE (2026-09-10). Des que le schema suit `serveur_forgejo_tls`, la +# sonde parle en HTTPS a `127.0.0.1` — alors que le certificat step-ca est emis pour le +# FQDN de la machine. La verification echoue, curl ne rend RIEN, et le test « sortie vide » +# ci-dessous traduisait cela en « la forge ne repond pas » : un CRITICAL sur une forge qui +# servait le genome au meme instant. +# +# Valider le certificat ici reviendrait a mesurer la PKI depuis le mauvais endroit — c'est +# le travail de la sonde `certificat`, qui compare le cert SERVI a celui du disque. Celle-ci +# demande a Forgejo ce qu'il pense de lui-meme ; le transport n'est pas son sujet. +sortie=$(curl -sk --max-time 10 "${URL}/api/healthz" 2>/dev/null) [[ -n "${sortie}" ]] || { echo "La forge ne repond pas sur ${URL}."; exit 2; } verdict=$(printf '%s' "${sortie}" | python3 -c ' diff --git a/roles/serveur_loki/defaults/main.yml b/roles/serveur_loki/defaults/main.yml index 89d8287..38cdb31 100644 --- a/roles/serveur_loki/defaults/main.yml +++ b/roles/serveur_loki/defaults/main.yml @@ -20,4 +20,16 @@ serveur_loki_tls_source_cert: "/etc/step/certs/{{ ansible_fqdn | default(ansible serveur_loki_tls_source_cle: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.key" # --- Sonde de supervision ----------------------------------------------------------- -serveur_loki_sonde_url: "https://127.0.0.1:{{ serveur_loki_port }}" +# LE SCHEMA SE DERIVE DU COMMUTATEUR TLS, il ne s'ecrit pas (mesure du 2026-09-10). +# +# Cette ligne disait `https` en dur, alors que `serveur_loki_tls_actif` vaut `false` par +# defaut. Chez le tenant, qui l'active dans ses group_vars, l'accord etait fortuit ; au +# site, qui ne l'active pas, la sonde interrogeait en HTTPS un Loki qui sert en clair et +# rapportait « Loki ne repond pas » — un CRITICAL sur un service en parfaite sante. +# +# Une fausse alarme est pire qu'une sonde absente : elle apprend a ne plus lire la sonde. +# Meme forme que le reglage d'authentification de Grafana, corrige le meme jour — un +# parametre qui ne suit pas l'interrupteur dont il depend. +serveur_loki_sonde_schema: "{{ 'https' if serveur_loki_tls_actif | bool else 'http' }}" +serveur_loki_sonde_url: >- + {{ serveur_loki_sonde_schema }}://127.0.0.1:{{ serveur_loki_port }} diff --git a/roles/serveur_ops/defaults/main.yml b/roles/serveur_ops/defaults/main.yml index 6cefdff..88b5aa5 100644 --- a/roles/serveur_ops/defaults/main.yml +++ b/roles/serveur_ops/defaults/main.yml @@ -186,4 +186,18 @@ serveur_ops_roues_depot: "{{ serveur_ops_racine }}/.roues-hors-ligne" # --- Sonde de supervision ----------------------------------------------------------- serveur_ops_sonde_racine: /opt/setops serveur_ops_sonde_venv: /opt/setops/venv +# LA BRANCHE ATTENDUE SE DECLARE PAR DEPOT, PAS UNE FOIS POUR TOUS (2026-09-10). +# +# `serveur_ops_depots` porte deja un `branche:` par entree — le plan du site declare +# `master` pour `set-ops-modeles` et `ops-technolibre`. La sonde comparait pourtant les six +# depots a une valeur unique, et rapportait donc une divergence CRITIQUE sur deux depots +# posos EXACTEMENT la ou le plan les veut. +# +# Une sonde qui contredit la declaration qu'elle est censee verifier n'accuse pas la +# machine : elle s'accuse elle-meme. On derive la table de ce que le plan dit, et +# `serveur_ops_sonde_branche` ne sert plus que de defaut aux entrees qui se taisent. serveur_ops_sonde_branche: main +serveur_ops_sonde_branches: >- + {{ dict(serveur_ops_depots | map(attribute='dest') + | zip(serveur_ops_depots + | map(attribute='branche', default=serveur_ops_sonde_branche))) }} diff --git a/roles/serveur_ops/templates/sonde-runner.sh.j2 b/roles/serveur_ops/templates/sonde-runner.sh.j2 index 2ec85e6..4785bdb 100644 --- a/roles/serveur_ops/templates/sonde-runner.sh.j2 +++ b/roles/serveur_ops/templates/sonde-runner.sh.j2 @@ -13,6 +13,13 @@ set -uo pipefail VENV={{ serveur_ops_sonde_venv }} BRANCHE={{ serveur_ops_sonde_branche }} +# La branche ATTENDUE de chaque depot, telle que le plan la declare. Un depot absent de +# cette table retombe sur `BRANCHE` — c'est le cas de ceux qui ne declarent rien. +declare -A BRANCHES=( +{% for _d, _b in (serveur_ops_sonde_branches | default({})).items() %} + ["{{ _d }}"]="{{ _b }}" +{% endfor %} +) RACINE={{ serveur_ops_sonde_racine }} # `safe.directory` : les depots appartiennent au compte d'exploitation, la sonde tourne en @@ -25,7 +32,8 @@ for d in "${RACINE}"/*/; do [[ -d "${d}.git" ]] || continue n=$((n+1)); nom=$(basename "${d}") b=$(git_ "${d}" rev-parse --abbrev-ref HEAD) - [[ "${b}" == "${BRANCHE}" ]] || ennuis+=("${nom} sur « ${b:-?} »") + attendue="${BRANCHES[${nom}]:-${BRANCHE}}" + [[ "${b}" == "${attendue}" ]] || ennuis+=("${nom} sur « ${b:-?} » au lieu de « ${attendue} »") # UN DEPOT SALE SUR LE RUNNER EST UNE DIVERGENCE SILENCIEUSE : ce qui s'applique a la # flotte n'est plus ce que le depot contient, et personne ne le sait. [[ -z "$(git_ "${d}" status --porcelain)" ]] || ennuis+=("${nom} MODIFIE localement") @@ -40,4 +48,4 @@ if (( ${#ennuis[@]} > 0 )); then echo "Runner : ${ennuis[*]} — ce qui s'applique diverge du depot.|depots=${n}" exit 2 fi -echo "Runner : ${n} depot(s) propres sur « ${BRANCHE} », ${v}.|depots=${n}" +echo "Runner : ${n} depot(s) propres sur leur branche declaree, ${v}.|depots=${n}" diff --git a/scripts/resoudre_flux.py b/scripts/resoudre_flux.py index 313d2ad..c4f61ef 100644 --- a/scripts/resoudre_flux.py +++ b/scripts/resoudre_flux.py @@ -417,6 +417,37 @@ def _ports_du_plan() -> dict[str, str]: return {} +def _ports_du_plan_site() -> dict[str, str]: + """Port declare par le plan du SITE pour chaque groupe — l'autre moitie de `derive`. + + LES DEUX PLANS NE DISENT PAS LA MEME CHOSE, ET C'EST TOUT L'INTERET (2026-09-10). + + `derive` veut dire « ce port depend du deploiement ». Forgejo en est l'exemple meme : + chez un tenant il ecoute 3000 DERRIERE un edge qui termine le TLS ; au site il n'a pas + d'edge et sert son propre TLS sur 443. Le plan du site le dit noir sur blanc : + + forgejo: + # 443, ET NON 3000. [...] garder 3000 aurait grave `:3000` dans ROOT_URL + + `generer_nftables(site=True)` interrogeait pourtant le plan du TENANT. La regle d'hote + de la forge du site ouvrait donc 3000 — un port que personne n'ecoute — et laissait 443 + ferme a toute la flotte du site. + + CE QUE CA A COUTE : le runner ne pouvait plus cloner le genome. Mesure du 2026-09-10, + depuis les sept machines, `10.0.33.11:443` etait injoignable de partout SAUF depuis la + forge elle-meme — y compris depuis une machine du MEME sous-reseau, ce qui excluait la + frontiere et designait le pare-feu de l'hote. Le tenant, lui, restait juste : son + Forgejo ecoute bien 3000. Un seul registre pour deux verites opposees. + """ + try: + import underlay as _u + apps = (_u.lire_plan_site("applications.yml") or {}).get("applications") or {} + return {str(a["groupe"]): str(a["port"]) for a in apps.values() + if a.get("groupe") and a.get("port")} + except Exception: + return {} + + def _inventaire_site() -> dict: """L'inventaire du SITE, rendu dans la forme qu'attendent les fonctions ci-dessus. @@ -497,7 +528,7 @@ def generer_nftables(flux_par_role: dict[str, list[dict]], site: bool = False) - admin_ssh = _sources_admin_ssh() actifs_noms = set(_hotes_du_groupe(data, "hotes_actifs")) ip_par_hote = _ip_par_hote(data) - ports_plan = _ports_du_plan() + ports_plan = _ports_du_plan_site() if site else _ports_du_plan() regles_admin = [ f" ip saddr {{ {cidr} }} tcp dport 22 accept # administration (garde anti-lockout, intrant nftables_admin_ssh)" for cidr in admin_ssh