From 231da8e020a3b5c47127587469a2ea0733449b52 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 8 Oct 2026 16:29:05 +0200 Subject: [PATCH 1/3] =?UTF-8?q?fix(crowdsec)=20=E2=80=94=20l'agent=20lit?= =?UTF-8?q?=20enfin=20les=20journaux=20de=20Traefik=20:=20la=20racine=20Do?= =?UTF-8?q?cker=20de=20pi1=20est=20mont=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur pi1, les journaux de conteneurs passent par une chaîne de liens qui finit sous /mnt/arcodange/docker/containers (racine Docker déplacée ; l'ancienne, /var/lib/docker/containers, n'est plus écrite depuis le 2026-04-07). Le chart ne montait que /var/log et l'ancienne racine : le lien pendait dans le pod. Mesuré le 2026-10-08 : « Error setting up tail for /var/log/containers/traefik-… » au démarrage de l'agent (2026-09-26), les 54 journaux de /var/log/pods pointent tous sous /mnt/arcodange/docker, et la métrique cs_filesource_hits_total n'existe pas (zéro ligne lue). Aucun scénario HTTP n'a donc jamais pu déborder, pendant que la forge était aspirée. Montage hostPath en lecture seule AU MÊME CHEMIN (le noyau résout le lien dans l'espace de montage du pod), type Directory pour que l'agent refuse de démarrer plutôt que de redevenir aveugle en silence si la racine bouge encore. Co-Authored-By: Claude Opus 5.5 --- crowdsec/values.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crowdsec/values.yaml b/crowdsec/values.yaml index 65fe23c..eb58cee 100644 --- a/crowdsec/values.yaml +++ b/crowdsec/values.yaml @@ -22,6 +22,34 @@ crowdsec: &crowdsec_config value: "crowdsecurity/traefik crowdsecurity/http-cve" - name: TZ value: Europe/Paris + # ⚠ L'AGENT NE LISAIT AUCUNE LIGNE DE TRAEFIK — mesuré le 2026-10-08. + # + # Sur pi1, Docker (le runtime de k3s ici) a sa racine de données sous + # /mnt/arcodange/docker. Le journal d'un conteneur se lit par une chaîne de + # liens : + # /var/log/containers/traefik-…log + # → /var/log/pods/kube-system_traefik-…/traefik/0.log + # → /mnt/arcodange/docker/containers//-json.log + # Le chart ne monte que /var/log et /var/lib/docker/containers (l'ancienne + # racine, plus écrite depuis le 2026-04-07). Le dernier lien pendait donc + # dans le pod : `head` sur le lien → « No such file or directory », les 54 + # journaux de /var/log/pods pointent tous sous /mnt/arcodange/docker, et la + # métrique `cs_filesource_hits_total` n'existait même pas (zéro ligne lue). + # Aucun scénario HTTP ne pouvait donc mordre, quel que soit le trafic. + # + # On monte la vraie racine, en lecture seule, AU MÊME CHEMIN : c'est le + # noyau qui résout le lien dans l'espace de montage du pod, le chemin doit + # donc exister tel quel. Si la racine Docker de pi1 bouge encore, ce + # montage doit suivre (relever `readlink` d'un 0.log de /var/log/pods). + extraVolumes: + - name: racine-docker-pi1 + hostPath: + path: /mnt/arcodange/docker/containers + type: Directory + extraVolumeMounts: + - name: racine-docker-pi1 + mountPath: /mnt/arcodange/docker/containers + readOnly: true lapi: # Source stable pour le CUSTOM_HOSTNAME défini plus bas : un fieldRef ne sait # lire qu'un champ du pod, et tous ceux que le chart expose varient (le nom) -- 2.54.0 From 601a2e7d84fd525d5e987f2b4df7331e3cb578b0 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 8 Oct 2026 16:29:17 +0200 Subject: [PATCH 2/3] =?UTF-8?q?feat(crowdsec)=20=E2=80=94=20deux=20sc?= =?UTF-8?q?=C3=A9narios=20contre=20l'aspiration=20de=20la=20forge,=20bans?= =?UTF-8?q?=20de=2024=20h,=20foyer=20en=20liste=20blanche?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le 2026-10-08, 16.216.88.0/24 (74 adresses qui tournent) et 74.7.227.0/24 ont fait tourner Gitea à 100 % plus d'une heure sur blame/commit/commits/raw. crowdsecurity/http-crawl-non_statics ne pouvait pas les voir : il compte par adresse et tolère 2 req/s soutenues ; la plage ne dépassait pas 9 req/min par adresse, et `.ts` (TypeScript ici) est rangé parmi les fichiers statiques. - arcodange/forge-aspiration : pages git coûteuses sur le routeur gitea, regroupées par /24 ou /64, capacité 60, fuite 4 s ; - arcodange/forge-comparaisons : compare/archive, capacité 15, fuite 60 s (un troisième robot enchaînait une comparaison de ~10 s toutes les 15 s) ; - portée Ip conservée (une alerte par adresse présente dans le seau) : le videur Traefik en mode stream n'applique pas les décisions de plage ; - profil LAPI : 24 h pour ces deux scénarios, les deux profils par défaut recopiés tels quels derrière ; - liste blanche des adresses publiques du foyer (IPv4 et /64 IPv6). Rejoué hors trafic dans un CrowdSec v1.7.0 local, avec le hub copié de l'agent du cluster : voir la PR (débordements, sabotage du seuil, lecteur humain, adresses privées et du foyer). Co-Authored-By: Claude Opus 5.5 --- crowdsec/values.yaml | 123 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/crowdsec/values.yaml b/crowdsec/values.yaml index eb58cee..10c5d36 100644 --- a/crowdsec/values.yaml +++ b/crowdsec/values.yaml @@ -181,6 +181,129 @@ crowdsec: &crowdsec_config cpu: "100m" memory: "200Mi" config: + # ── Aspiration de la forge publique (gitea.arcodange.fr) ────────────────── + # Le 2026-10-08, deux plages (16.216.88.0/24, 74 adresses qui tournent, et + # 74.7.227.0/24) ont fait tourner Gitea (pi2, 4 cœurs) à 100 % pendant plus + # d'une heure sur blame/commit/commits/raw, à ~1 req/s, 3,8 s de Gitea par + # requête. `crowdsecurity/http-crawl-non_statics` ne pouvait pas les voir : + # il compte PAR ADRESSE et tolère 2 req/s soutenues (fuite 0,5 s), alors que + # la plage la plus lourde ne dépassait jamais 9 req/min par adresse ; et il + # range les `.ts` (TypeScript, ici) parmi les fichiers statiques. + # + # D'où deux seaux qui fuient, réservés au routeur de la forge : + # - ils ne comptent que les pages qui font travailler git ; + # - ils regroupent par /24 (IPv4) ou /64 (IPv6), pour voir le robot qui + # répartit sa charge sur une plage ; + # - mais leur PORTÉE reste l'adresse (scope Ip, la valeur par défaut) : + # au débordement, CrowdSec émet une alerte — donc un ban — par adresse + # présente dans le seau. Ne PAS passer en `scope: Range` : le videur de + # Traefik (plugin crowdsec-bouncer-traefik, mode stream) ne range ses + # décisions que par valeur exacte, un ban de plage n'y bloque personne + # (mesuré le 2026-10-08 sur les bans manuels des deux plages). + # - les réponses 403 (déjà bannies par le videur) ne sont pas recomptées, + # pour laisser la place, au débordement suivant, aux adresses de la même + # plage qui passent encore. + # Seuils : une personne qui lit l'historique fait quelques pages par minute + # et n'approche pas 60 d'affilée au-dessus de 15/min ; le robot le plus lent + # du 2026-10-08 (24 req/min) déborde en ~7 min, la plage (51 req/min) en + # moins de 2 min. Les rejeux qui le prouvent (et le sabotage du seuil) sont + # dans la PR qui a introduit ces lignes : `git log -S forge-aspiration`. + scenarios: + forge-aspiration.yaml: | + type: leaky + name: arcodange/forge-aspiration + description: "Aspiration des pages git couteuses de la forge (src/commit, commits, blame, raw...), comptee par /24 ou /64" + filter: | + evt.Meta.log_type == 'http_access-log' + && evt.Meta.traefik_router_name contains 'gitea' + && evt.Parsed.verb in ['GET', 'HEAD'] + && evt.Meta.http_status != '403' + && evt.Parsed.request matches '^/[^/]+/[^/]+/(src/(commit|branch|tag)|commits|commit|blame|compare|raw|archive|media|graph|activity|lastcommit)(/|$)' + groupby: "IpToRange(evt.Meta.source_ip, IsIPV4(evt.Meta.source_ip) ? '/24' : '/64')" + capacity: 60 + leakspeed: 4s + blackhole: 1m + labels: + service: http + behavior: "http:crawl" + label: "Aspiration de la forge" + confidence: 2 + spoofable: 0 + classification: + - attack.T1595 + remediation: true + # Une comparaison (`/compare/a...b`) ou une archive coûte à elle seule ~10 s + # de Gitea : le 2026-10-08, un troisième robot en enchaînait une toutes les + # 15 s (dont 3 sur 4 abandonnées avant la réponse, statut 499) — trop lent + # pour le seau du dessus, assez pour occuper un cœur en permanence. + forge-comparaisons.yaml: | + type: leaky + name: arcodange/forge-comparaisons + description: "Rafale de comparaisons ou d'archives sur la forge, comptee par /24 ou /64" + filter: | + evt.Meta.log_type == 'http_access-log' + && evt.Meta.traefik_router_name contains 'gitea' + && evt.Parsed.verb in ['GET', 'HEAD'] + && evt.Meta.http_status != '403' + && evt.Parsed.request matches '^/[^/]+/[^/]+/(compare|archive)(/|$)' + groupby: "IpToRange(evt.Meta.source_ip, IsIPV4(evt.Meta.source_ip) ? '/24' : '/64')" + capacity: 15 + leakspeed: 60s + blackhole: 1m + labels: + service: http + behavior: "http:crawl" + label: "Comparaisons en rafale sur la forge" + confidence: 2 + spoofable: 0 + classification: + - attack.T1595 + remediation: true + parsers: + s02-enrich: + # `crowdsecurity/whitelists` couvre déjà 10/8, 172.16/12, 192.168/16 et + # 127/8 (les runners de CI et ArgoCD passent par gitea.arcodange.lab, + # donc par une adresse privée). On y ajoute les adresses PUBLIQUES du + # foyer, par lesquelles on revient quand on passe par Cloudflare. + # ⚠ Toutes deux sont dynamiques (relevées le 2026-07-28, voir + # doc/ce-que-traefik-voit.md) : si elles changent, c'est ici. + foyer-whitelist.yaml: | + name: arcodange/foyer-whitelist + description: "Adresses publiques du foyer : jamais bannies" + whitelist: + reason: "adresse publique du foyer (homelab)" + ip: + - "86.238.234.54" + cidr: + - "2a01:cb04:dff:cf00::/64" + # Les deux premiers profils de l'image, recopiés tels quels, précédés d'un + # profil qui bannit 24 h (au lieu de 4 h) ce que la forge a vu aspirer : un + # robot d'aspiration revient, et chaque retour coûte un débordement complet. + # ⚠ Ce fichier REMPLACE celui de l'image : un profil retiré d'ici n'existe plus. + profiles.yaml: | + name: forge_aspiration_24h + filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() startsWith "arcodange/forge-" + decisions: + - type: ban + duration: 24h + on_success: break + --- + name: default_ip_remediation + filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" + decisions: + - type: ban + duration: 4h + on_success: break + --- + name: default_range_remediation + filters: + - Alert.Remediation == true && Alert.GetScope() == "Range" + decisions: + - type: ban + duration: 4h + on_success: break config.yaml.local: | db_config: type: postgresql -- 2.54.0 From 8052b533da0731d4dad1b05c38eabfaea5579e6a Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 8 Oct 2026 16:29:17 +0200 Subject: [PATCH 3/3] =?UTF-8?q?doc(r=C3=A9seau)=20=E2=80=94=20ce=20que=20l?= =?UTF-8?q?e=20videur=20juge=20bien,=20et=20les=20deux=20limites=20qu'on?= =?UTF-8?q?=20ne=20voyait=20pas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La section 6 disait que la convention « crowdsec devant tout » tenait parce que le videur juge la vraie IP. C'est vrai, mais le 2026-10-08 a montré deux limites : l'agent ne lisait aucune ligne (racine Docker non montée), et le plugin en mode stream ignore les décisions de plage (vérifié : la LAPI rend bien la décision de plage en mode live, le flux stream la livre comme une valeur que le plugin ne compare qu'à l'égalité). Co-Authored-By: Claude Opus 5.5 --- doc/ce-que-traefik-voit.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/ce-que-traefik-voit.md b/doc/ce-que-traefik-voit.md index 4f3d474..dc18ea9 100644 --- a/doc/ce-que-traefik-voit.md +++ b/doc/ce-que-traefik-voit.md @@ -211,6 +211,26 @@ mais à l'IP résolue. La convention `.fr` (« crowdsec devant tout ») tient do ce qu'elle promet. Écrit ici pour que personne ne « corrige » cette ligne en croyant reproduire le piège A. +> ⚠ **Mise à jour du 2026-10-08 — juger la bonne IP ne suffisait pas.** Deux +> limites que ce constat ne pouvait pas voir, mesurées pendant l'aspiration de +> la forge : +> +> 1. **L'agent ne lisait rien.** Les journaux de conteneurs de pi1 pointent sous +> `/mnt/arcodange/docker` (racine Docker déplacée), que l'agent ne montait +> pas : `Error setting up tail for /var/log/containers/traefik-…` à chaque +> démarrage, aucune ligne lue, aucun scénario HTTP n'a jamais débordé. +> Corrigé dans `crowdsec/values.yaml` (`agent.extraVolumes`). +> 2. **Un ban de plage n'est pas appliqué.** En mode `stream`, le plugin +> (v1.3.3 ici, et toujours sur sa branche principale) range chaque décision +> par sa valeur exacte et cherche l'adresse du client telle quelle : +> `cscli decisions add --range 16.216.88.0/24` laissait passer +> `16.216.88.122` (`isBanned:false cache:miss`). Les scénarios maison +> gardent donc une portée **Ip**, et un ban manuel de plage se fait adresse +> par adresse (`cscli decisions import`). Le mode `live` interrogerait la +> LAPI (`/v1/decisions?ip=…`), qui, elle, rend la décision de plage pour une +> adresse qu'elle contient (vérifié sur une LAPI v1.7.0 locale) — au prix +> d'un appel par adresse nouvelle. + --- **Où vit quoi** (aucun de ces réglages n'est dans ce dépôt — vérifié) : -- 2.54.0