portabilite : le repli nftables survivait a la bascule et annulait tout

Le socle pose l'un de DEUX fichiers dans /etc/nftables.conf : le ruleset
derive (table setops_flux) s'il existe, sinon un gabarit de repli plat (table
setops_filter) qui n'ouvre que le 22. Ce sont des ALTERNATIVES, jamais des
couches.

Mais le rechargement est `nft -f`, qui AJOUTE sans purger, et le fichier
derive retirait setops_flux — jamais setops_filter. Un hote passe du repli au
derive gardait donc DEUX chaines input sur le meme hook, toutes deux en
policy drop. Le paquet traverse les deux : seule l'intersection de leurs
accept passait.

Symptome parfaitement trompeur : AC debout, 8443 en ecoute, regle posee et
acceptante, ICMP entre les deux VM a 0,15 ms — et step ca bootstrap qui
expire. Il a fallu lire le ruleset entier pour voir la seconde table.

Le fichier derive retire desormais les DEUX tables (le repli portait deja
flush ruleset). Pas de flush ruleset cote derive : choix du depot pour ne pas
detruire de tables etrangeres, respecte.

Ce qui l'avait rendu possible : `make reconstruire` ne generait JAMAIS les
flux. `make flux` en est maintenant la premiere etape.

Et `no_log` a masque la cause trois fois dans la journee — dont au terme d'un
deploiement de 157 taches. Les taches concernees extraient desormais le
verdict a part (ce qui a echoue, quel code, quel message) sans toucher aux
en-tetes. Une garde qui protege un secret ne doit pas emporter le diagnostic.

Verifie : TLS OK depuis infra-dns-01 vers l'AC, une seule table nftables,
13/14 hotes deployes sans echec, prouver.py 34 OK, ansible-lint production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-10 12:24:41 -04:00
parent 3b16b8845d
commit 9c1c790f9d
4 changed files with 85 additions and 4 deletions

View file

@ -56,6 +56,44 @@ rien à faire ». Le verrou de la seule commande destructive du moteur passait d
sans rien éprouver. Le faux cluster **fabrique désormais la collision sur le plan courant**, sans rien éprouver. Le faux cluster **fabrique désormais la collision sur le plan courant**,
quel qu'il soit. quel qu'il soit.
### 4. Le repli nftables survivait à la bascule — et annulait tout
Le socle pose l'un de **deux** fichiers dans `/etc/nftables.conf` : le ruleset **dérivé**
(`make flux`, table `setops_flux`) s'il existe, sinon un **gabarit de repli** plat (table
`setops_filter`) qui n'ouvre que le `22`. Ce sont des **alternatives**, jamais des couches.
Mais le rechargement est `nft -f`, qui **ajoute sans purger**, et le fichier dérivé retirait
soigneusement `setops_flux`… jamais `setops_filter`. Un hôte passé du repli au dérivé se
retrouvait donc avec **deux chaînes `input` sur le même hook, toutes deux en `policy drop`**.
Le paquet traverse les deux : seule l'**intersection** de leurs `accept` passait — le `22` et
l'ICMP, rien d'autre.
Le symptôme était parfaitement trompeur : l'AC **debout**, son port 8443 **en écoute**, sa
règle `ip saddr { … } tcp dport 8443 accept` **posée et acceptante**, l'ICMP entre les deux
VM à **0,15 ms** — et `step ca bootstrap` qui expire. Il a fallu lire le ruleset entier pour
voir la seconde table.
Le fichier dérivé retire désormais **les deux** tables (le repli, lui, portait déjà
`flush ruleset`). Pas de `flush ruleset` côté dérivé : c'est un choix du dépôt pour ne pas
détruire de tables étrangères, et il est respecté.
**Ce qui l'avait rendu possible** : `make reconstruire` ne générait **jamais** les flux.
Sans eux, `flux-genere/` est vide, le repli est posé, et l'hôte passe ensuite au dérivé —
exactement la bascule qui casse. `make flux` est maintenant la première étape de
`reconstruire`. Invisible sur un écosystème déjà construit, dont les `.nft` traînent d'une
exécution précédente.
### `no_log` a masqué la cause trois fois dans la même journée
Le clonage, l'attente de configuration, et la déclaration des URI de déconnexion Keycloak :
trois échecs lus « the output has been hidden », dont un au terme d'un déploiement de 157
tâches. Le mot-clé protège de vrais secrets — un jeton d'API porté par un en-tête — et on ne
peut pas simplement l'enlever.
Les trois tâches **extraient donc désormais le verdict à part** : ce qui a échoué, avec quel
code et quel message, sans jamais toucher aux en-têtes. Une garde qui protège un secret ne
doit pas emporter le diagnostic avec lui.
### Ce qui relevait des données du tenant, pas du moteur ### Ce qui relevait des données du tenant, pas du moteur
L'instance datait d'avant plusieurs évolutions, et les preuves statiques les ont toutes L'instance datait d'avant plusieurs évolutions, et les preuves statiques les ont toutes

View file

@ -594,6 +594,14 @@ _attendre-flotte: ansible-runtime
raser: ansible-runtime _instance-requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom> raser: ansible-runtime _instance-requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom>
@python3 scripts/raser.py $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer) @python3 scripts/raser.py $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer)
# `make flux` D'ABORD, et ce n'est pas une precaution : sans lui, `instance/flux-genere/`
# est VIDE et le socle deploie nftables en `policy drop` SANS AUCUNE REGLE DERIVEE. La
# flotte se monte, SSH repond depuis l'administration — et tout le reste est mur.
#
# Invisible sur un ecosysteme deja construit, dont les .nft trainaient d'une execution
# precedente. Trouve le 2026-08-10 en montant Technolibre depuis zero : l'AC etait
# debout, son port 8443 en ecoute, et `step ca bootstrap` expirait depuis une VM du
# meme sous-reseau. Zero regle pour 8443 dans le ruleset, `flux-genere/` vide.
reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet
@set -e; \ @set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \ if [[ "$(CONFIRMER)" != "true" ]]; then \
@ -602,6 +610,7 @@ reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM pu
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \ printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
exit 2; \ exit 2; \
fi; \ fi; \
$(MAKE) flux; \
$(MAKE) flotte-creer CONFIRMER=true; \ $(MAKE) flotte-creer CONFIRMER=true; \
$(MAKE) _attendre-flotte; \ $(MAKE) _attendre-flotte; \
$(MAKE) _amorcer-socle; \ $(MAKE) _amorcer-socle; \

View file

@ -48,9 +48,31 @@
loop: "{{ serveur_keycloak_clients_actuels.json }}" loop: "{{ serveur_keycloak_clients_actuels.json }}"
loop_control: loop_control:
label: "{{ item.clientId }}" label: "{{ item.clientId }}"
# `no_log` protege le JETON porte par l'en-tete — on ne peut pas simplement l'enlever.
# Mais il masquait aussi la CAUSE : le 2026-08-10, l'echec s'est lu « the output has
# been hidden » sur le tout dernier appel d'un deploiement de 157 taches. On journalise
# donc le verdict a part, en n'extrayant que ce qui ne revele rien.
no_log: true no_log: true
register: serveur_keycloak_deconnexion_res
failed_when: false
changed_when: true changed_when: true
when: when:
- item.clientId in serveur_keycloak_deconnexion - item.clientId in serveur_keycloak_deconnexion
- (item.attributes | default({})).get('post.logout.redirect.uris', '') - (item.attributes | default({})).get('post.logout.redirect.uris', '')
!= serveur_keycloak_deconnexion[item.clientId] != serveur_keycloak_deconnexion[item.clientId]
- name: Dire quels clients Keycloak a refusés, et pourquoi
ansible.builtin.fail:
msg: >-
Keycloak a refuse la mise a jour des URI de deconnexion pour :
{{ refuses | map(attribute='item.clientId') | list | join(', ') }}.
Codes rendus : {{ refuses | map(attribute='status') | list }}.
Messages : {{ refuses | map(attribute='msg') | map('string')
| map('truncate', 160) | list }}.
vars:
refuses: >-
{{ (serveur_keycloak_deconnexion_res.results | default([]))
| rejectattr('skipped', 'defined')
| selectattr('status', 'defined')
| rejectattr('status', 'equalto', 204) | list }}
when: refuses | length > 0

View file

@ -290,11 +290,23 @@ def _rendre_nft(hote: str, regles: list[str]) -> str:
return ( return (
f"#!/usr/sbin/nft -f\n" f"#!/usr/sbin/nft -f\n"
f"# GENERE par scripts/resoudre_flux.py depuis les meta/flux.yml — hote {hote}.\n" f"# GENERE par scripts/resoudre_flux.py depuis les meta/flux.yml — hote {hote}.\n"
f"# APERCU inspectable — NON active. L'activation reste un geste dedie, teste par noeud.\n\n" f"# Deploye par le role `nftables_baseline` en tant que /etc/nftables.conf.\n\n"
f"# Remplace UNIQUEMENT la table setops (pas de `flush ruleset` : préserve les\n" f"# Remplace UNIQUEMENT les tables setops (pas de `flush ruleset` : préserve les\n"
f"# tables de Docker — DNAT/forward des conteneurs, ex. Collabora).\n" f"# tables etrangeres — DNAT/forward de conteneurs, par exemple).\n"
f"#\n"
f"# `setops_filter` est le gabarit de REPLI, pose quand aucun ruleset resolu\n"
f"# n'existe encore. Les deux fichiers sont des ALTERNATIVES, jamais des couches —\n"
f"# mais `nft -f` AJOUTE sans purger, et le repli survivait donc a la bascule.\n"
f"# Deux chaines `input` sur le meme hook, toutes deux en `policy drop` : le\n"
f"# paquet traverse les DEUX, et seule l'intersection de leurs `accept` passait.\n"
f"# Mesure du 2026-08-10 sur Technolibre : l'AC etait debout, sa regle 8443 posee\n"
f"# et acceptante, l'ICMP passait en 0,15 ms — et le TCP expirait, arrete par le\n"
f"# `policy drop` du repli. Chaque fichier retire donc desormais la table de\n"
f"# l'autre, et la bascule converge dans les deux sens.\n"
f"table inet setops_flux {{}}\n" f"table inet setops_flux {{}}\n"
f"delete table inet setops_flux\n\n" f"delete table inet setops_flux\n"
f"table inet setops_filter {{}}\n"
f"delete table inet setops_filter\n\n"
f"table inet setops_flux {{\n" f"table inet setops_flux {{\n"
f" chain input {{\n" f" chain input {{\n"
f" type filter hook input priority 0; policy drop;\n\n" f" type filter hook input priority 0; policy drop;\n\n"