Compare commits
3
Commits
259861fb41
...
8052b533da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8052b533da | ||
|
|
601a2e7d84 | ||
|
|
231da8e020 |
@@ -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
|
||||
|
||||
@@ -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é) :
|
||||
|
||||
Reference in New Issue
Block a user