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]>
This commit is contained in:
CI Bot
2026-10-08 16:29:17 +02:00
co-authored by Claude Opus 5.5
parent 231da8e020
commit 601a2e7d84
+123
View File
@@ -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