fix(traefik,crowdsec) — plugin bouncer lu sur disque : un hoquet de 10 s coupait tout l'ingress #62

Merged
arcodange merged 1 commits from arcodange/crowdsec-repair-045e72 into main 2026-09-20 18:39:47 +02:00
Owner

Ce qui s'est passé

Le 2026-09-20 à 15:00:50, Traefik a redémarré et n'a pas réussi à télécharger le plugin bouncer CrowdSec :

unable to install plugin crowdsec-bouncer: ... Get
"https://plugins.traefik.io/.../v1.3.3": context deadline exceeded

Il ne s'arrête pas pour autant. Il journalise Plugins are disabled because an error has occurred. en ERR puis sert quand même — sans plugin. Le middleware crowdsec devient alors invalid middleware type or middleware does not exist, et Traefik jette tous les routeurs qui le référencent, c'est-à-dire presque tout l'ingress : kadans, cms, webapp, url-shortener, minio, telegram-gateway, prospection, grafana, gitea, plausible.

Mesuré au diagnostic :

Constat Valeur
www.arcodange.lab, cms-rec.arcodange.lab 404 pendant 3 h 18
Dernier pull du bouncer 14:30:50, soit ~3 h 45 avant
Erreurs invalid middleware dans les logs Traefik en continu, sur chaque routeur
Latence de plugins.traefik.io depuis un pod de pi1 214 ms

Le registre répondait normalement au moment du diagnostic : ce n'était pas une panne, juste un hoquet de moins de 10 s. Ça a suffi à ouvrir l'edge et à couper les sites. Un kubectl rollout restart a tout rétabli (404 → 200) — c'est la remise en service immédiate, faite avant ce correctif.

Pourquoi c'est arrivé

1. Le réseau était dans le chemin de démarrage

experimental.plugins fait passer Traefik par RegistryDownloader.Download(), qui appelle plugins.traefik.io à chaque démarrage, avec 10 s de timeout.

⚠ La correction évidente — persister /plugins-storage, aujourd'hui un emptyDir — est fausse. Vérifié dans pkg/plugins/downloader.go (v3.6.2) : l'appel HTTP est fait inconditionnellement, le cache ne sert qu'à envoyer un hash pour obtenir un 304. Avoir l'archive sur disque n'aurait rien évité.

2. L'échec était silencieux

Servir l'ingress sans bouncer ni captcha, en ne le signalant que dans les logs, n'est pas un mode dégradé acceptable pour un composant de sécurité.

Ce que fait cette PR

experimental.localPlugins — un autre chemin de code (SetupLocalPlugins), qui ne fait aucune I/O réseau : Yaegi lit les sources sous /plugins-local/src/<module>. Elles sont posées sur un PVC par un Job Ansible, à la version déclarée. Le réseau ne sert plus qu'au provisionnement, où l'on peut réessayer (5 tentatives) sans que l'ingress soit en jeu.

abortOnPluginFailure: true — Traefik ≥ 3.3 sait refuser de démarrer plutôt que de servir sans plugin. Maintenant qu'il n'y a plus de réseau au boot, ce drapeau ne peut se déclencher que sur un PVC vide ou corrompu : cas où l'on veut précisément qu'il refuse de démarrer.

Même famille de fragilité que 93e7abf (résolution de la forge, 2026-08-26) : on ne fait pas dépendre l'edge d'un service joignable à la seconde près.

⚠ Monter la version du plugin passe désormais par Ansible (--tags traefik-plugins) et non plus par un champ que Traefik irait chercher. Le Job est idempotent (marqueur .vendored-version), tourne sur le nœud du control-plane et monte le PVC RWO en parallèle de Traefik — il peut donc repeupler sans couper l'ingress, la nouvelle version prenant effet au redémarrage suivant.

Vérification

Appliqué en prod (ansible-playbook playbooks/system/k3s_config.yml), puis mesuré :

Contrôle Avant Après
Chargement du plugin Loading plugins… 15:00:40 → échec 15:00:50 (10 s de timeout) Loading plugins… → Plugins loaded. dans la même seconde
Erreurs invalid middleware en continu 0
www.arcodange.lab / cms-rec.arcodange.lab 404 / 404 200 / 200
grafana / gitea 302 / 200 302 / 200
Pull du bouncer figé à 14:30 18:37:38, en continu
Montage /plugins-storage emptyDir supprimé
  • Test de non-régression : rollout restart de Traefik — le scénario exact qui cassait. Pod neuf, plugin chargé dans la seconde, 0 middleware invalide, routes toujours à 200.
  • PVC : v1.3.3, 107 fichiers, 1 Mo, .traefik.yml + vendor/ + go.mod présents, lisibles par l'UID 65532 de Traefik.
  • Rendu : helm template du chart v37.4.0 avec les valeurs réelles du playbook, avant application.
  • Pas de dérive collatérale : --tags longhorn --check --diff → ok, le playbook ne touche que Traefik.

Ce que je n'ai pas exercé : un blocage réel par le bouncer. clientTrustedIPs couvre 192.168.1.0/24 et 10.42.0.0/16, donc toute requête que je peux émettre (LAN ou pod) est whitelistée par construction. La preuve du chemin d'application reste le pull des décisions depuis la LAPI, qui tourne.

Hors périmètre, constaté au passage

  • 594 machines fantômes dans la base de la LAPI : chaque redémarrage du pod LAPI enregistre une machine de plus (depuis 2025-12) et rien ne les purge.
  • La LAPI avait crashé plus tôt sur FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute — saturation de max_connections (100) sur le Postgres de pi2. Deux identifiants dynamiques Vault de plausible détenaient à eux seuls 20 connexions idle : pgbouncer ouvre un pool par couple (user, base), et chaque rotation Vault en crée un nouveau sans fermer l'ancien.

Ces deux points sont réels mais distincts de la panne d'ingress, et leur cause racine est dans le dépôt tools, pas ici.

🤖 Generated with Claude Code

## Ce qui s'est passé Le 2026-09-20 à 15:00:50, Traefik a redémarré et n'a pas réussi à télécharger le plugin bouncer CrowdSec : ``` unable to install plugin crowdsec-bouncer: ... Get "https://plugins.traefik.io/.../v1.3.3": context deadline exceeded ``` **Il ne s'arrête pas pour autant.** Il journalise `Plugins are disabled because an error has occurred.` en ERR puis sert quand même — sans plugin. Le middleware `crowdsec` devient alors `invalid middleware type or middleware does not exist`, et Traefik **jette tous les routeurs qui le référencent**, c'est-à-dire presque tout l'ingress : kadans, cms, webapp, url-shortener, minio, telegram-gateway, prospection, grafana, gitea, plausible. Mesuré au diagnostic : | Constat | Valeur | |---|---| | `www.arcodange.lab`, `cms-rec.arcodange.lab` | **404** pendant 3 h 18 | | Dernier pull du bouncer | 14:30:50, soit ~3 h 45 avant | | Erreurs `invalid middleware` dans les logs Traefik | en continu, sur chaque routeur | | Latence de `plugins.traefik.io` depuis un pod de pi1 | **214 ms** | Le registre répondait normalement au moment du diagnostic : ce n'était pas une panne, juste un hoquet de moins de 10 s. Ça a suffi à ouvrir l'edge et à couper les sites. Un `kubectl rollout restart` a tout rétabli (404 → 200) — c'est la remise en service immédiate, faite avant ce correctif. ## Pourquoi c'est arrivé ### 1. Le réseau était dans le chemin de démarrage `experimental.plugins` fait passer Traefik par `RegistryDownloader.Download()`, qui appelle `plugins.traefik.io` **à chaque démarrage**, avec 10 s de timeout. > ⚠ La correction évidente — persister `/plugins-storage`, aujourd'hui un `emptyDir` — **est fausse**. Vérifié dans `pkg/plugins/downloader.go` (v3.6.2) : l'appel HTTP est fait *inconditionnellement*, le cache ne sert qu'à envoyer un hash pour obtenir un 304. Avoir l'archive sur disque n'aurait rien évité. ### 2. L'échec était silencieux Servir l'ingress sans bouncer ni captcha, en ne le signalant que dans les logs, n'est pas un mode dégradé acceptable pour un composant de sécurité. ## Ce que fait cette PR **`experimental.localPlugins`** — un autre chemin de code (`SetupLocalPlugins`), qui ne fait **aucune I/O réseau** : Yaegi lit les sources sous `/plugins-local/src/<module>`. Elles sont posées sur un PVC par un Job Ansible, à la version déclarée. Le réseau ne sert plus qu'au provisionnement, où l'on peut réessayer (5 tentatives) sans que l'ingress soit en jeu. **`abortOnPluginFailure: true`** — Traefik ≥ 3.3 sait refuser de démarrer plutôt que de servir sans plugin. Maintenant qu'il n'y a plus de réseau au boot, ce drapeau ne peut se déclencher que sur un PVC vide ou corrompu : cas où l'on veut précisément qu'il refuse de démarrer. Même famille de fragilité que 93e7abf (résolution de la forge, 2026-08-26) : on ne fait pas dépendre l'edge d'un service joignable à la seconde près. > ⚠ **Monter la version du plugin passe désormais par Ansible** (`--tags traefik-plugins`) et non plus par un champ que Traefik irait chercher. Le Job est idempotent (marqueur `.vendored-version`), tourne sur le nœud du control-plane et monte le PVC RWO en parallèle de Traefik — il peut donc repeupler **sans couper l'ingress**, la nouvelle version prenant effet au redémarrage suivant. ## Vérification Appliqué en prod (`ansible-playbook playbooks/system/k3s_config.yml`), puis mesuré : | Contrôle | Avant | Après | |---|---|---| | Chargement du plugin | `Loading plugins…` 15:00:40 → **échec 15:00:50** (10 s de timeout) | `Loading plugins…` → `Plugins loaded.` **dans la même seconde** | | Erreurs `invalid middleware` | en continu | **0** | | `www.arcodange.lab` / `cms-rec.arcodange.lab` | 404 / 404 | **200 / 200** | | `grafana` / `gitea` | 302 / 200 | 302 / 200 | | Pull du bouncer | figé à 14:30 | **18:37:38**, en continu | | Montage `/plugins-storage` | `emptyDir` | **supprimé** | - **Test de non-régression** : `rollout restart` de Traefik — le scénario exact qui cassait. Pod neuf, plugin chargé dans la seconde, 0 middleware invalide, routes toujours à 200. - **PVC** : `v1.3.3`, 107 fichiers, 1 Mo, `.traefik.yml` + `vendor/` + `go.mod` présents, lisibles par l'UID 65532 de Traefik. - **Rendu** : `helm template` du chart v37.4.0 avec les valeurs réelles du playbook, avant application. - **Pas de dérive collatérale** : `--tags longhorn --check --diff` → `ok`, le playbook ne touche que Traefik. Ce que je **n'ai pas** exercé : un blocage réel par le bouncer. `clientTrustedIPs` couvre `192.168.1.0/24` et `10.42.0.0/16`, donc toute requête que je peux émettre (LAN ou pod) est whitelistée par construction. La preuve du chemin d'application reste le pull des décisions depuis la LAPI, qui tourne. ## Hors périmètre, constaté au passage - **594 machines fantômes** dans la base de la LAPI : chaque redémarrage du pod LAPI enregistre une machine de plus (depuis 2025-12) et rien ne les purge. - **La LAPI avait crashé plus tôt** sur `FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute` — saturation de `max_connections` (100) sur le Postgres de pi2. Deux identifiants dynamiques Vault de `plausible` détenaient à eux seuls 20 connexions *idle* : pgbouncer ouvre un pool par couple (user, base), et chaque rotation Vault en crée un nouveau sans fermer l'ancien. Ces deux points sont réels mais distincts de la panne d'ingress, et leur cause racine est dans le dépôt `tools`, pas ici. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
arcodange added 1 commit 2026-09-20 18:39:12 +02:00
Le 2026-09-20 à 15:00:50, Traefik a redémarré et n'a pas réussi à télécharger
le plugin bouncer CrowdSec :

  unable to install plugin crowdsec-bouncer: ... Get
  "https://plugins.traefik.io/.../v1.3.3": context deadline exceeded

Il ne s'arrête pas pour autant. Il journalise « Plugins are disabled because an
error has occurred. » et sert quand même — sans plugin. Le middleware `crowdsec`
devient alors « invalid middleware type or middleware does not exist » et Traefik
JETTE tous les routeurs qui le référencent, c'est-à-dire presque tout l'ingress.

Mesuré : www.arcodange.lab et cms-rec.arcodange.lab en 404 pendant 3 h 18, et
zéro pull du bouncer depuis 14:30. Un `kubectl rollout restart` a tout rétabli
(404 → 200), ce qui confirme le caractère transitoire : le registre répondait en
214 ms depuis un pod de pi1 au moment du diagnostic. Un hoquet de moins de 10 s
a suffi à ouvrir l'edge et à couper les sites.

Deux causes, deux correctifs.

1. Le réseau était dans le chemin de démarrage (experimental.localPlugins)

`experimental.plugins` fait passer Traefik par RegistryDownloader.Download(),
qui appelle plugins.traefik.io à CHAQUE démarrage, avec 10 s de timeout.

⚠ Avoir déjà l'archive en cache NE DISPENSE PAS de l'appel : vérifié dans
pkg/plugins/downloader.go (v3.6.2), le cache ne sert qu'à envoyer un hash pour
obtenir un 304. Persister /plugins-storage (aujourd'hui un emptyDir) n'aurait
donc rien évité — c'était la correction évidente, elle est fausse.

`localPlugins` emprunte un autre chemin de code, SetupLocalPlugins, qui ne fait
aucune I/O réseau : Yaegi lit les sources sous /plugins-local/src/<module>. Les
sources sont posées sur un PVC par un Job Ansible, à la version déclarée.
Le réseau ne sert plus qu'au provisionnement, où l'on peut réessayer sans que
l'ingress soit en jeu (5 tentatives).

Même famille de fragilité que 2026-08-26 (résolution de la forge) : on ne fait
pas dépendre l'edge d'un service joignable à la seconde près.

2. L'échec était silencieux (abortOnPluginFailure)

Servir l'ingress sans bouncer ni captcha, en ne le disant qu'en INFO dans les
logs, n'est pas un mode dégradé acceptable pour un composant de sécurité.
Traefik >= 3.3 sait refuser de démarrer : on le lui demande. Maintenant qu'il
n'y a plus de réseau au boot, ce drapeau ne peut se déclencher que sur un PVC
vide ou corrompu — cas où l'on veut précisément qu'il refuse de démarrer.

⚠ Monter la version du plugin passe désormais par Ansible
(`--tags traefik-plugins`) et non plus par un champ que Traefik irait chercher.
Le Job est idempotent (marqueur .vendored-version) et tourne sur le nœud du
control-plane, donc il peut repeupler le PVC sans couper l'ingress.

Vérifié : `helm template` du chart v37.4.0 avec les valeurs réelles du playbook
rend bien --experimental.localPlugins.crowdsec-bouncer.moduleName et
--experimental.abortonpluginfailure=true, le montage en subPath, et plus aucun
emptyDir `plugins`.

Co-Authored-By: Claude Opus 5 <[email protected]>
arcodange merged commit c2aab58b86 into main 2026-09-20 18:39:47 +02:00
arcodange deleted branch arcodange/crowdsec-repair-045e72 2026-09-20 18:39:48 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: arcodange-org/factory#62