3 Commits
Author SHA1 Message Date
CI BotandClaude Opus 5.5 8052b533da doc(réseau) — ce que le videur juge bien, et les deux limites qu'on ne voyait pas
Helm Charts / Detect changed charts (pull_request) Successful in 39s
Helm Charts / Library charts tool (pull_request) Skipped
Helm Charts / Application charts alloy (pull_request) Skipped
Helm Charts / Application charts chart (pull_request) Skipped
Helm Charts / Application charts grafana (pull_request) Skipped
Helm Charts / Application charts hashicorp-vault (pull_request) Skipped
Helm Charts / Application charts loki (pull_request) Skipped
Helm Charts / Application charts minio (pull_request) Skipped
Helm Charts / Application charts pgbouncer (pull_request) Skipped
Helm Charts / Application charts pgcat (pull_request) Skipped
Helm Charts / Application charts prometheus (pull_request) Skipped
Helm Charts / Application charts redis (pull_request) Skipped
Helm Charts / Application charts crowdsec (pull_request) Successful in 1m0s
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 <[email protected]>
2026-10-08 16:29:17 +02:00
CI BotandClaude Opus 5.5 601a2e7d84 feat(crowdsec) — deux scénarios contre l'aspiration de la forge, bans de 24 h, foyer en liste blanche
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 <[email protected]>
2026-10-08 16:29:17 +02:00
CI BotandClaude Opus 5.5 231da8e020 fix(crowdsec) — l'agent lit enfin les journaux de Traefik : la racine Docker de pi1 est montée
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 <[email protected]>
2026-10-08 16:29:05 +02:00
2 changed files with 171 additions and 0 deletions
+151
View File
@@ -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/<id>/<id>-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)
@@ -153,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
+20
View File
@@ -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é) :