Mutualiser une « whitelist » d'infrastructures de confiance (CHATONS, Fediverse, Libre)

Miaouw tout le monde !

En travaillant sur la sécurité de mes services, je me suis fait une réflexion que je voulais partager avec vous : et si on mettait en commun nos connaissances pour construire une base de données d’infrastructures de confiance (CHATONS, Fediverse, logiciels libres) qu’on pourrait utiliser comme whitelist partagée ?

Le constat : le bruit de fond n’est pas normal

Quand on gère des serveurs, on s’habitue tous au « bruit de fond » : scans automatisés, tentatives d’intrusion, bots qui cherchent des failles… On voit les logs défiler, Fail2Ban ou le firewall bloquent, et on passe à autre chose.

Mais à force de recevoir des milliers de tentatives, il suffit d’une porte mal fermée ou d’une faille récente pour que ça passe.

Pour prendre une image : Internet ressemble à une porte d’immeuble où des milliers de personnes essaient des clés au hasard. Ce n’est pas parce qu’elles échouent presque toutes qu’il faut arrêter de regarder qui essaie d’entrer.

L’idée : Partager ce que nous savons déjà être légitime

Plutôt que d’essayer de lister tout ce qui est malveillant (les blacklists deviennent vite obsolètes ou bloquent des sous-réseaux entiers par erreur), pourquoi ne pas répertorier ce qu’on sait être légitime ?

Les CHATONS connaissent leurs adresses IP. Le Fediverse connaît ses instances. Les projets libres connaissent leurs serveurs.

L’idée serait de centraliser ces adresses/domaines de confiance dans une base communautaire, claire et traçable (savoir qui a déclaré l’entrée, quand, et avec quel niveau de vérification).

À quoi ça servirait ?

Chacun pourrait piocher dans cette base (via API ou fichiers exportés) pour alimenter ses propres outils (firewalls, reverse-proxies, scripts maison…).

Concrètement, cela permet d’indiquer à nos systèmes :

« Cette IP appartient à une infrastructure connue : évite de la considérer automatiquement comme hostile simplement parce qu’elle génère du trafic inhabituel. »

Cela permet de rendre nos blocages automatisés plus agressifs contre les vrais indésirables, sans risquer de casser la fédération ou d’isoler des services légitimes.

Éviter les pièges : pas de confiance aveugle, pas d’autorité centrale

Trois points majeurs pour que le projet reste sain :

  1. Pas d’effet « chèque en blanc » : Une IP peut changer de main ou une machine être compromise. Chaque entrée doit avoir une date de vérification, une durée de vie ou un score de confiance.
  2. Pas d’autorité centrale : Le but n’est pas de décréter qui est « bon » ou « mauvais », mais de mutualiser des informations déclarées, sourcées et vérifiables.
  3. Liberté totale : Chacun reste maître de sa politique de sécurité et choisit ce qu’il intègre ou non.

Qu’en pensez-vous ?

Avant de réfléchir à l’architecture technique ou d’écrire du code, je voulais savoir si le besoin est partagé.

Si l’idée vous parle, je serais curieux d’avoir vos retours :

  • Est-ce un besoin que vous ressentez aussi ?
  • Quelles données/infrastructures vous paraîtraient utiles à y inclure ?
  • Quels précautions ou risques voyez-vous ?

Au plaisir de lire vos avis !

1 Like

Coucou ! Merci pour cète proposition :slight_smile:

De mon point de vue d’ébergeuse, et de mainteneuse de reaction, j’ai plutôt l’impression que le problème principal quand on blockliste une IP, c’est qu’on risque de bloquer l’IP d’un client légitime, plutôt que d’un serveur.
De mon expérience, il arive que deus serveurs que je gère se bloquent, mais dans ce cas-là, c’est assez facile à débugger et ça n’impacte pas trop les « clients » (au sens réseau).

Est-ce que tu as rencontré des cas où tu bloquais les IPs d’infrastructures amies ?

Remarques un peu hors-sujet
  • +1 sur le fait qu’utiliser aveuglément des blocklists de tiers nuit à l’Internet ! Allons joyeusement bruler Spamhaus ensemble :stuck_out_tongue:

  • Perso je préfère les termes blocklist/allowlist plutôt que blacklist/whitelist, c’est plus descriptif et moins chargé istoriquement[1]

1 Like

Coucou !

Merci pour ton retour ! L’idée de cette allowlist (tu as raison, c’est bien plus adapté comme terme !) m’est venue justement pour résoudre ce problème de « dégâts collatéraux » quand on veut se protéger efficacement.

Sur mon infra, les blocages d’IP uniques via Fail2ban posent rarement problème. Le souci arrive quand l’attaque est distribuée ou massive depuis un même hébergeur.

Voici mon fonctionnement actuel et le problème concret qui en découle :

  • Fail2ban détecte une activité anormale (par exemple un scan de fichiers sensibles).

  • L’IP est ajoutée temporairement à la blocklist de mon routeur.

  • Si plusieurs attaques successives proviennent du même réseau, mon script monte d’un cran pour éviter de traiter les IP une par une : il bloque le sous-réseau complet (voire l’ASN).

Le problème provient justement de ce dernier point.

Par exemple, si je dois bloquer un sous-réseau très bruyant chez Scaleway (comme 51.158.0.0/15), je risque de couper l’accès à une instance légitime hébergée au même endroit, comme masto.bike (51.158.37.47). Je dis si mais c’est arrivé (j’ai bloqué cette instance amie par accident)

Aujourd’hui, je contourne ça avec une allowlist locale sur mon routeur : j’y entre les noms de domaine légitimes, et le routeur résout leurs IP régulièrement pour s’assurer qu’elles restent accessibles, même si le bloc réseau global est filtré.

Actuellement, je fais tourner un script qui cartographie les instances du Fediverse pour en extraire les IP et alimenter cette allowlist. Mais c’est du bricolage dans mon coin : certaines instances partagent des IP (derrière des reverse proxies), d’autres bougent et ce serait tellement plus efficace de mutualiser cette connaissance à l’échelle des CHATONS.

Bref, l’objectif n’est pas de bloquer les clients/utilisateurs finaux, mais bien de pouvoir bloquer méchamment les réseaux d’hébergeurs devenus de vraies passoires (comme DigitalOcean ou Scaleway ou Fastly ou Amazon ou Microsoft Azure ça va j’arrête lol) sans couper les copains qui y louent un VPS !

1 Like

Je savais que j’avais oublié de dire quelque chose : depuis peu j’ai créé quelques pages wiki pour essayer de documenter Mes Numériques, (j’y mettrai des tutos et de la doc sur l’infra), il y a une page qui se met à jour en temps réel, à chaque fois que mon routeur blocklist un réseau, certains diront que c’est excessif et je répondrai « OUI ! », mais j’accueille du public et contrairement à « France Passoire » je mets toutes les chances de mon côté pour protéger les données que l’on m’a confiées, comme si c’étaient les miennes. securite:banlist – Mes Numériques

Lui c’est le pompon :

64.23.218.208 - - [23/Aug/2026:03:04:09 +0200] « GET /server HTTP/1.1 » 404 899 « - » « Mozilla/5.0 (l9scan/2.0.5393e26393e27363e22383; +https://leakix.net) »
64.23.218.208 - - [23/Aug/2026:03:04:10 +0200] « GET /server-status HTTP/1.1 » 404 904 « - » « Mozilla/5.0 (l9scan/2.0.5393e26393e27363e22383; +https://leakix.net) »
64.23.218.208 - - [23/Aug/2026:03:04:12 +0200] « GET /about HTTP/1.1 » 404 899 « - » « Mozilla/5.0 (l9scan/2.0.5393e26393e27363e22383; +https://leakix.net) »
64.23.218.208 - - [23/Aug/2026:03:04:13 +0200] « GET /login.action HTTP/1.1 » 404 904 « - » « Mozilla/5.0 (l9scan/2.0.5393e26393e27363e22383; +https://leakix.net) »
64.23.218.208 - - [23/Aug/2026:03:04:15 +0200] « GET /___proxy_subdomain_whm/login HTTP/1.1 » 404 917 « - » « Mozilla/5.0 (l9scan/2.0.5393e26393e27363e22383; +https://leakix.net) »
64.23.218.208 - - [23/Aug/2026:03:04:15 +0200] « GET /___proxy_subdomain_cpanel HTTP/1.1 » 404 914 « - » « Mozilla/5.0 (l9scan/2.0.5393e26393e27363e22383; +https://leakix.net) »
64.23.218.208 - - [23/Aug/2026:03:04:16 +0200] « GET /v2/_catalog HTTP/1.1 » 404 905 « - » « Mozilla/5.0 (l9scan/2.0.5393e26393e27363e22383; +https://leakix.net) »
64.23.218.208 - - [23/Aug/2026:03:04:17 +0200] « GET /.DS_Store HTTP/1.1 » 404 902 « - » « Mozilla/5.0 (l9scan/2.0.5393e26393e27363e22383; +https://leakix.net) »
64.23.218.208 - - [23/Aug/2026:03:04:18 +0200] « GET /.env HTTP/1.1 » 404 899 « - » « Mozilla/5.0 (l9scan/2.0.5393e26393e27363e22383; +https://leakix.net) »

Lui il a l’honnêteté de publier son user-agent et il confirme que c’est un outil de scan qui cherche des failles. L’heureux propriétaire de cette IP est ? *ੈ✩‧₊˚ DigitalOcean *ੈ✩‧₊˚ bien sûr !

L’IP est consignée dans dans la blocklist (j’ai regardé on peut dire denylist) du routeur, IP unique donc aucune répercussion sur un user légitime (logiquement)

image

Si le vilain utilise plusieurs IP (ou ils sont plusieurs vilains) dans la même plage d’IP, ça finit ici.

image

Et pour limiter les dégâts d’un tel bannissement, il y a un script qui envoie au routeur, les domaines du Fediverse qu’il découvre (dans allowlist), il analyse mes listes de bans pour voir si certaines instances ne sont pas déjà bloquées, si c’est le cas il dé-ban. Dans la liste des exceptions on peut voir les réseaux qui sont bloqués dont les instances font partie.

On peut voir sur cette page pas mal d’instances qui se trouvent sur un même réseau, j’ai commencé à travailler sur le problème le 19 août. On va prendre l’exemple des serveurs derrière 194.164.48.x

image

Date du blocage de l’hébergeur IONOS SE le 25 juillet, ce qui signifie que toutes les instances vues plus haut ont été injoignables pendant presque 1 mois.

Salut,
Je gère une 50 aine de serveurs pour environs 500 site internets hébergés.
Ta réflexion je l’ai eu aussi mais à l’inverse, des ban list plutôt que de liste blanche (bien trop restrictives).
J’ai du passer sur Nftables et abandonner Iptables trop peu performant quand les millions d’IP et CIDR s’accumulent.

Les faux positifs se produisent inévitablement et génère de l’insatisfaction et du travail.
Le recours aux RZO de proxy rotatif résidentiel c’est démocratisé depuis 2 ans (coucou les IA ? )
Aujourd’hui je ne bloque plus que certains AS clairement merdiques ou bullet proof.
J’ai Crowdsec installé partout (meilleurs perf et détection que F2B même si non totalement libre).
Et j’encaisse la charge pour reste :frowning:

Salut @greg ,

Merci pour ton partage d’expérience. Je suis aussi de ton avis. Les faux positifs, il y en aura c’est inévitable, mais dire qu’on laisse couler parce que ça devient vite ingérable, j’ai du mal à m’y résoudre. Je gère une infra minuscule où je fais tourner quelques services, la plupart des utilisateurs qui s’y connectent sont des particuliers comme moi, ceux là je ne m’inquiète pas pour eux, sauf cas exceptionnels d’utilisateurs accros aux VPN. :wink:

Ce qui m’embête un peu, ce sont les serveurs qui communiquent directement avec les miens, quand c’est de l’auto-hébergement à domicile ou au bureau derrière une IP réelle ça passe (j’ai répertorié pas mal de réseaux d’entreprise autre que Free, Bouygues, SFR, Orange), mais dès qu’ils sont hébergés dans des méga-structures, ça devient vite compliqué. (un jour je les aurai toutes ?)

La peur des serveurs mails bloqués : ça n’est jamais arrivé, parce que je les ai exclus eux-aussi et qu’en général les services d’hébergement ne se trouvent pas sur les mêmes réseaux que les autres services. Par exemple, tu ne trouveras jamais une IP Gmail sur une plage d’IP Google Cloud. Et c’est là tout l’intérêt d’avoir une liste alimentée par des CHATONS, allé au hasard un fournisseur mail ? Je le mets en exception.

C’est long, fastidieux, imprécis mais c’est mieux que rien. Qu’est-ce que je risque vraiment ?

Fail2ban je m’en sers comme détecteur, il ne sert qu’à déclencher des scripts, c’est le routeur qui encaisse tout, laissant mes machines se concentrer sur autre chose.

Tu me parles de AS merdiques, il y en a beaucoup, du moment qu’un hébergeur propose des solutions pas chères, parfois même des essais gratuits et qu’en plus de ça ils sont performants, c’est là que tu trouveras les bots et pirates les plus agressifs (j’exclue les machines corrompues).

:1st_place_medal: 1 DigitalOcean Il est partout

:2nd_place_medal: 2 Google Logique j’ai envie de dire

:3rd_place_medal: 3 Tencent Cloud Médaille de bronze mais serré avec ceux qui suivent

4 UCLOUD c’est beaucoup de l’Asie

5 Microsoft Azure un vrai poison international

6 Amazon AWS idem

7 Alibaba Cloud plus agressif que dans le passé

8 Akamai trop tôt pour en parler mais il est là

9 Datacamp beaucoup dans l’anonymisation

Je vois ta situation.
Dans ce cas pourquoi ne pas tout interdire par défaut et autoriser uniquement certain AS.
En gros les AS des FAI français et un Belge.

J’ai quelque machines configurées comme ça, tranquillité absolue :relieved:
Un script met à jour les CIDR des AS chaque dimanche.

Au cas cela t’intéresse je te passe ma conf nftable à adapter avec IPset du coup.
Nftables écrit par la même personne que Iptables est stable depuis 15 ans et corrige tout les défauts (nombreux) de Iptables.
Il m’a falu un bon moment pour me mettre à la logique un peu différentes, je manipule Iptables depuis tellement longtemps… ça a été dur :smiley:

La conf à adapter (je n’ai pas d’Iptables désolé) :

  #!/usr/sbin/nft -f
  flush ruleset

  # ============================================================
  # 1. whitelist (priorite -20) - passe avant tout le reste
  # ============================================================
  table inet whitelist {
        set admin_whitelist4 {
                type ipv4_addr
                flags interval
                elements = { 51.38.215.32, 80.13.223.198, 127.0.0.1 }
        }

        set monitor_whitelist4 {
                type ipv4_addr
                flags interval
                elements = { 45.90.160.153, 94.143.138.32 }
        }

        set ovh_monitor_whitelist4 {
                type ipv4_addr
                flags interval
                elements = { }
        }

        set system_whitelist4 {
                type ipv4_addr
                flags interval
                elements = { 1.1.1.1, 8.8.8.8 }
        }

        set client_whitelist4 {
                type ipv4_addr
                flags interval
        }

        chain input {
                type filter hook input priority -20; policy accept;
                ip saddr @admin_whitelist4 accept
                ip saddr @monitor_whitelist4 accept
                ip saddr @ovh_monitor_whitelist4 accept
                ip saddr @system_whitelist4 accept
                ip saddr @client_whitelist4 accept
        }
  }
    
  # ============================================================
  # 3. blacklist-geo (priorite -15) - blocage pays
  # ============================================================
  table inet blacklist-geo {
        set country4-ir {
                type ipv4_addr
                flags interval
                # elements = { ... }   # 1740 prefixes (Iran) - `fw update-country`
        }
        set country6-ir {
                type ipv6_addr
                flags interval
                # elements = { ... }   # 570 prefixes
        }
        set country4-kp {
                type ipv4_addr
                flags interval
                elements = { 5.62.56.160/30, 31.6.16.15,
                             57.73.214.0/23, 61.149.72.86,
                             175.45.176.0/22, 185.130.45.98,
                             194.50.99.122, 194.164.173.174,
                             196.48.114.0/24, 196.196.114.0/24,
                             196.197.114.0/24, 196.199.114.0/24,
                             210.52.109.0/24 }
        }
        set country6-kp { type ipv6_addr; flags interval; }
  
        chain input {
                type filter hook input priority -15; policy accept;
                ip saddr @country4-ir drop
                ip6 saddr @country6-ir drop
                ip saddr @country4-kp drop
                ip6 saddr @country6-kp drop
        }
  }
  
  # ============================================================
  # 7. firewall-main (priorite 0, policy drop)
  # ============================================================
  table inet firewall-main {
        set dns4-accept {
                type ipv4_addr
                flags interval
                elements = { 1.1.1.1, 8.8.4.4, 8.8.8.8, 9.9.9.9 }
        }

        set portscan4-drop {
                type ipv4_addr
                flags dynamic,timeout
                timeout 90d
        }
  
        set portscan6-drop {
                type ipv6_addr
                flags dynamic,timeout
                timeout 90d
        }

        chain input {
                type filter hook input priority filter; policy drop;
                iif "lo" accept
                ct state established,related accept
                ct state invalid drop
                ip saddr @portscan4-drop drop
                ip6 saddr @portscan6-drop drop
                icmp type echo-request limit rate 10/second burst 5 packets accept
                icmpv6 type echo-request limit rate 10/second burst 5 packets accept
                tcp dport { 80, 443 } accept
                tcp dport 250 accept
                udp sport 67 udp dport 68 accept
                limit rate 5/minute burst 5 packets log prefix "[nftables] DROP IN: " level info
                counter drop
        }
  
        chain forward {
                type filter hook forward priority filter; policy drop;
                counter drop
        }

        chain output {
                type filter hook output priority filter; policy drop;
                ct state established,related accept
                ip daddr @dns4-accept udp dport 53 accept
                ip daddr @dns4-accept tcp dport 53 accept
                ip daddr 127.0.0.53 udp dport 53 accept
                ip daddr 127.0.0.53 tcp dport 53 accept
                udp dport 123 accept
                tcp dport { 80, 443 } accept
                tcp dport { 25, 465, 587 } accept
                tcp dport 250 accept 
                udp sport 68 udp dport 67 accept
                icmp type { echo-reply, echo-request } accept
                icmpv6 type { echo-request, echo-reply } accept
                udp dport 33434-33534 accept
                meta skuid 0 accept
                limit rate 5/minute burst 5 packets log prefix "[nftables] DROP OUT: " level info
                counter drop
        }       
  }     
  
  # ============================================================
  # 8. whitelist-isp (priorite -10) - genere par update-as-whitelist.sh
  # ============================================================
  include "/etc/nftables.d/whitelist-isp.nft"

Le script de mise jour update-as-whitelist.sh :

  #!/bin/bash
  #===============================================================================
  # Script: update-as-whitelist.sh
  # Description: Met à jour les sets nftables avec les préfixes des FAI français
  # Date: 2025-12-12
  #===============================================================================
  
  set -euo pipefail

  #-------------------------------------------------------------------------------
  # CONFIGURATION
  #-------------------------------------------------------------------------------

  # Liste des AS des FAI français à autoriser
  # Format: "AS_NUMBER:DESCRIPTION"
  declare -a AS_LIST=(
      "3215:Orange"
      "12322:Free"
      "51207:Free_Mobile"
      "15557:SFR"
      "5410:Bouygues"
      "21502:SFR_Business"
      "6799:Numericable"
      "35540:OVH_Telecom"
      "5432:Proximus"
      "199636:Free_Pro"
      "8220:COLT"
      "16347:ADISTA"
  )

  # Fichiers et répertoires
  NFT_OUTPUT_FILE="/etc/nftables.d/whitelist-isp.nft"
  NFT_OUTPUT_DIR="/etc/nftables.d"
  LOG_FILE="/var/log/update-as-whitelist.log"
  TMP_DIR="/tmp/as-whitelist-$$"

  # Configuration mail
  MAIL_TO="xxx@xx.fr"
  MAIL_FROM="hosname@xxx.com"
  HOSTNAME=$(hostname -f)

  # API RIPEstat
  RIPESTAT_API="https://stat.ripe.net/data/announced-prefixes/data.json"
  API_TIMEOUT=30

  # Mode
  DRY_RUN=false
  VERBOSE=false

  #-------------------------------------------------------------------------------
  # FONCTIONS
  #-------------------------------------------------------------------------------

  log() {
      local level="$1"
      shift
      local message="$*"
      local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
      echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
  }

  log_info() { log "INFO" "$@"; }
  log_warn() { log "WARN" "$@"; }
  log_error() { log "ERROR" "$@"; }

  send_error_mail() {
      local subject="$1"
      local body="$2"

      log_error "Envoi mail d'erreur à $MAIL_TO"
  
      echo -e "Subject: [ALERTE] $subject\nFrom: $MAIL_FROM\nTo: $MAIL_TO\n\n$body\n\n-- \nServeur: $HOSTNAME\nDate: $(date)\nLog: $LOG_FILE" | sendmail -t
  }

  cleanup() {
      rm -rf "$TMP_DIR" 2>/dev/null || true
  }

  trap cleanup EXIT
  
  usage() {
      cat << EOF
  Usage: $(basename "$0") [OPTIONS]

  Met à jour les sets nftables avec les préfixes des FAI français.

  Options:
      -n, --dry-run    Mode simulation (ne modifie rien)
      -v, --verbose    Mode verbeux
      -h, --help       Affiche cette aide

  Exemple:
      $(basename "$0") --dry-run    # Teste sans appliquer
      $(basename "$0")              # Applique les changements
  EOF
      exit 0
  }
  
  # Récupère les préfixes d'un AS depuis RIPEstat
  fetch_as_prefixes() {
      local as_number="$1"
      local ip_version="$2"  # 4 ou 6
      local output_file="$3"
  
      local url="${RIPESTAT_API}?resource=AS${as_number}"
      local response
      local prefixes

      response=$(curl -s --max-time "$API_TIMEOUT" "$url" 2>/dev/null) || {
          log_error "Échec requête API pour AS${as_number}"
          return 1
      }

      # Vérifier que la réponse est valide
      if ! echo "$response" | jq -e '.data.prefixes' >/dev/null 2>&1; then
          log_error "Réponse API invalide pour AS${as_number}"
          return 1
      fi
  
      # Extraire les préfixes IPv4 ou IPv6 directement dans le fichier
      if [ "$ip_version" = "4" ]; then
          echo "$response" | jq -r '.data.prefixes[].prefix' | grep -v ":" > "$output_file" 2>/dev/null || true
      else
          echo "$response" | jq -r '.data.prefixes[].prefix' | grep ":" > "$output_file" 2>/dev/null || true
      fi
  
      # Nettoyer les lignes vides
      sed -i '/^$/d' "$output_file" 2>/dev/null || true

      local count=$(wc -l < "$output_file" 2>/dev/null || echo "0")
      log_info "AS${as_number}: $count préfixes IPv${ip_version} récupérés"
  
      return 0
  }

  # Agrège les préfixes pour éliminer les chevauchements
  aggregate_prefixes() {
      local input_file="$1"
      local output_file="$2"
      local ip_version="$3"
  
      # Nettoyer le fichier d'entrée (supprimer lignes vides)
      sed -i '/^$/d' "$input_file" 2>/dev/null || true

      if [ ! -s "$input_file" ]; then
          : > "$output_file"
          return 0
      fi

      if [ "$ip_version" = "4" ]; then
          # IPv4 : utiliser aggregate (paquet Debian)
          sort -u "$input_file" | aggregate -q > "$output_file" 2>/dev/null || {
              # Fallback: pas d'agrégation si erreur
              sort -u "$input_file" > "$output_file"
          }
      else
          # IPv6 : utiliser Python ipaddress.collapse_addresses()
          python3 << PYTHON_SCRIPT > "$output_file"
  import ipaddress
  import sys

  prefixes = []
  with open("$input_file", "r") as f:
      for line in f:
          line = line.strip()
          if line:
              try:
                  prefixes.append(ipaddress.ip_network(line, strict=False))
              except ValueError:
                  pass

  if prefixes:
      collapsed = list(ipaddress.collapse_addresses(prefixes))
      for net in sorted(collapsed, key=lambda x: (x.network_address, x.prefixlen)):
          print(net)
  PYTHON_SCRIPT
      fi

      # Nettoyer le fichier de sortie
      sed -i '/^$/d' "$output_file" 2>/dev/null || true
  }

  # Génère le fichier nftables complet avec table et filtrage
  generate_nft_file() {
      local output_file="$1"
      local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

      cat > "$output_file" << EOF
  #!/usr/sbin/nft -f
  # Fichier généré automatiquement par update-as-whitelist.sh
  # NE PAS MODIFIER MANUELLEMENT
  # Dernière mise à jour: $timestamp
  #
  # Whitelist des FAI français - Filtrage par AS
  # Priorité -10 : après whitelist admin (-20), avant firewall-main (0)
  #
  # Liste des AS autorisés:
  EOF
  
      # Ajouter la liste des AS en commentaire
      for as_entry in "${AS_LIST[@]}"; do
          local as_number="${as_entry%%:*}"
          local as_desc="${as_entry##*:}"
          echo "# - AS$as_number ($as_desc)" >> "$output_file"
      done

      echo "" >> "$output_file"
      echo "table inet whitelist-isp {" >> "$output_file"
  
      # Générer les sets pour chaque AS
      for as_entry in "${AS_LIST[@]}"; do
          local as_number="${as_entry%%:*}"
          local as_desc="${as_entry##*:}"

          local v4_file="${TMP_DIR}/as${as_number}_v4_aggregated.txt"
          local v6_file="${TMP_DIR}/as${as_number}_v6_aggregated.txt"

          # Set IPv4
          echo "" >> "$output_file"
          echo "    # $as_desc (AS$as_number) - IPv4" >> "$output_file"
          echo "    set as4_${as_number} {" >> "$output_file"
          echo "        type ipv4_addr" >> "$output_file"
          echo "        flags interval" >> "$output_file"

          if [ -s "$v4_file" ]; then
              echo "        elements = {" >> "$output_file"
              sed 's/$/,/' "$v4_file" | sed '$ s/,$//' | sed 's/^/            /' >> "$output_file"
              echo "        }" >> "$output_file"
          fi
          echo "    }" >> "$output_file"

          # Set IPv6
          echo "" >> "$output_file"
          echo "    # $as_desc (AS$as_number) - IPv6" >> "$output_file"
          echo "    set as6_${as_number} {" >> "$output_file"
          echo "        type ipv6_addr" >> "$output_file"
          echo "        flags interval" >> "$output_file"

          if [ -s "$v6_file" ]; then
              echo "        elements = {" >> "$output_file"
              sed 's/$/,/' "$v6_file" | sed '$ s/,$//' | sed 's/^/            /' >> "$output_file"
              echo "        }" >> "$output_file"
          fi
          echo "    }" >> "$output_file"
      done
  
      # Set pour les IP admin (copie de la whitelist admin)
      cat >> "$output_file" << 'ADMIN_SETS'

      # IP administrateurs - toujours autorisées (copie de inet whitelist)
      set admin_whitelist4 {
          type ipv4_addr
          flags interval
          elements = {
              127.0.0.1,
              xxx.xx.xx.xx,
              xxx.xx.xx.xx  
          }
      }
  
      # IP monitoring - toujours autorisées
      set monitor_whitelist4 {
          type ipv4_addr
          flags interval
          elements = {
              xx.xx.xxx.xxx, xx.xx.xxx.xxx
          }
      }

      # OVH monitoring
      set ovh_monitor_whitelist4 {
          type ipv4_addr
          flags interval
          elements = {
              92.222.184.0/24,
              92.222.185.0/24,
              92.222.186.0/24,
              167.114.37.0/24,
              139.99.1.144/28,
              193.70.125.118/32,
              148.113.25.1/32,
              139.99.187.247/32,
              72.251.7.222/32,
              51.195.135.163/32,
              51.38.117.56/32,
              51.38.25.100/32,
              57.130.4.212/32,
              213.186.33.13/32
          }
      }
  ADMIN_SETS
  
      # Générer la chaîne input avec le filtrage
      cat >> "$output_file" << 'CHAIN_START'

      # Chaîne de filtrage - DROP si pas dans un AS français ou whitelist admin
      chain input {
          type filter hook input priority -10; policy accept;

          # Laisser passer le trafic local
          iif lo accept

          # Laisser passer les connexions établies
          ct state established,related accept

          # Laisser passer ICMP (ping)
          meta l4proto icmp accept
          meta l4proto icmpv6 accept

          # Laisser passer les IP admin/monitoring
          ip saddr @admin_whitelist4 accept
          ip saddr @monitor_whitelist4 accept
          ip saddr @ovh_monitor_whitelist4 accept
  
  CHAIN_START

      # Générer les règles accept pour chaque AS (IPv4)
      echo "        # === Whitelist FAI français IPv4 ===" >> "$output_file"
      for as_entry in "${AS_LIST[@]}"; do
          local as_number="${as_entry%%:*}"
          local as_desc="${as_entry##*:}"
          echo "        ip saddr @as4_${as_number} accept  # $as_desc" >> "$output_file"
      done

      echo "" >> "$output_file"
      echo "        # === Whitelist FAI français IPv6 ===" >> "$output_file"
      for as_entry in "${AS_LIST[@]}"; do
          local as_number="${as_entry%%:*}"
          local as_desc="${as_entry##*:}"
          echo "        ip6 saddr @as6_${as_number} accept  # $as_desc" >> "$output_file"
      done
  
      # Fin de la chaîne : drop le reste
      cat >> "$output_file" << 'CHAIN_END'

          # Tout le reste est droppé (IP non françaises)
          # Note: Les IP admin sont passées avant (whitelist priorité -20)
          log prefix "[WHITELIST-ISP] DROP: " level info
          counter drop
      }
  }
  CHAIN_END
  }

  #-------------------------------------------------------------------------------
  # MAIN
  #-------------------------------------------------------------------------------

  # Parse arguments
  while [[ $# -gt 0 ]]; do
      case "$1" in
          -n|--dry-run) DRY_RUN=true; shift ;;
          -v|--verbose) VERBOSE=true; shift ;;
          -h|--help) usage ;;
          *) log_error "Option inconnue: $1"; usage ;;
      esac
  done
  
  # Démarrage
  log_info "=========================================="
  log_info "Début de la mise à jour des whitelist AS"
  log_info "Mode: $([ "$DRY_RUN" = true ] && echo "DRY-RUN" || echo "PRODUCTION")"
  log_info "=========================================="
  
  # Créer le répertoire temporaire
  mkdir -p "$TMP_DIR"
  mkdir -p "$NFT_OUTPUT_DIR"

  # Variable pour tracker les erreurs
  HAD_ERRORS=false
  ERROR_DETAILS=""

  # Récupérer et agréger les préfixes pour chaque AS
  for as_entry in "${AS_LIST[@]}"; do
      as_number="${as_entry%%:*}"
      as_desc="${as_entry##*:}"

      log_info "Traitement de $as_desc (AS$as_number)..."

      # IPv4
      v4_raw="${TMP_DIR}/as${as_number}_v4_raw.txt"
      v4_agg="${TMP_DIR}/as${as_number}_v4_aggregated.txt"

      if ! fetch_as_prefixes "$as_number" "4" "$v4_raw"; then
          HAD_ERRORS=true
          ERROR_DETAILS="${ERROR_DETAILS}\n- Échec récupération IPv4 pour AS${as_number} ($as_desc)"
      else
          aggregate_prefixes "$v4_raw" "$v4_agg" "4"
          count_raw=$(wc -l < "$v4_raw" 2>/dev/null | tr -d ' ' || echo "0")
          count_agg=$(wc -l < "$v4_agg" 2>/dev/null | tr -d ' ' || echo "0")
          log_info "  IPv4: $count_raw préfixes -> $count_agg après agrégation"
      fi

      # IPv6
      v6_raw="${TMP_DIR}/as${as_number}_v6_raw.txt"
      v6_agg="${TMP_DIR}/as${as_number}_v6_aggregated.txt"

      if ! fetch_as_prefixes "$as_number" "6" "$v6_raw"; then
          HAD_ERRORS=true
          ERROR_DETAILS="${ERROR_DETAILS}\n- Échec récupération IPv6 pour AS${as_number} ($as_desc)"
      else
          aggregate_prefixes "$v6_raw" "$v6_agg" "6"
          count_raw=$(wc -l < "$v6_raw" 2>/dev/null | tr -d ' ' || echo "0")
          count_agg=$(wc -l < "$v6_agg" 2>/dev/null | tr -d ' ' || echo "0")
          log_info "  IPv6: $count_raw préfixes -> $count_agg après agrégation"
      fi
  done
  # En cas d'erreur, ne rien modifier et alerter
  if [ "$HAD_ERRORS" = true ]; then
      log_error "Des erreurs sont survenues, aucune modification effectuée"
      send_error_mail "Échec mise à jour whitelist AS sur $HOSTNAME" \
          "Le script de mise à jour des whitelist AS a rencontré des erreurs.\n\nDétails:${ERROR_DETAILS}\n\nAucune modification n'a été appliquée au firewall.\nLes règles actuelles restent en place."
      exit 1
  fi

  # Générer le fichier nftables
  NFT_TMP="${TMP_DIR}/whitelist-isp.nft"
  log_info "Génération du fichier nftables..."
  generate_nft_file "$NFT_TMP"

  # Valider la syntaxe
  # Note: nft -c valide contre le ruleset existant, donc on doit d'abord
  # sauvegarder et supprimer la table existante pour éviter les conflits
  log_info "Validation de la syntaxe nftables..."

  BACKUP_TABLE="${TMP_DIR}/whitelist-isp-backup.nft"
  nft list table inet whitelist-isp > "$BACKUP_TABLE" 2>/dev/null || true
  nft delete table inet whitelist-isp 2>/dev/null || true

  NFT_RESULT=0
  NFT_ERROR=$(nft -c -f "$NFT_TMP" 2>&1) || NFT_RESULT=$?

  # Restaurer la table existante après validation
  if [ -s "$BACKUP_TABLE" ]; then
      nft -f "$BACKUP_TABLE" 2>/dev/null || true
  fi

  if [ $NFT_RESULT -ne 0 ]; then
      log_error "Erreur de syntaxe dans le fichier généré: $NFT_ERROR"
      send_error_mail "Erreur syntaxe nftables sur $HOSTNAME" \
          "Le fichier nftables généré contient des erreurs de syntaxe.\n\nErreur: $NFT_ERROR\n\nAucune modification n'a été appliquée."
      exit 1
  fi
  
  log_info "Syntaxe validée avec succès"

  # Statistiques
  total_v4=$(cat ${TMP_DIR}/*_v4_aggregated.txt 2>/dev/null | grep -c . || echo "0")
  total_v6=$(cat ${TMP_DIR}/*_v6_aggregated.txt 2>/dev/null | grep -c . || echo "0")
  log_info "Total: $total_v4 préfixes IPv4, $total_v6 préfixes IPv6"

  # Appliquer ou afficher
  if [ "$DRY_RUN" = true ]; then
      log_info "[DRY-RUN] Fichier généré mais non appliqué"
      log_info "[DRY-RUN] Contenu:"
      echo "---"
      cat "$NFT_TMP"
      echo "---"
  else
      # Sauvegarder l'ancien fichier
      if [ -f "$NFT_OUTPUT_FILE" ]; then
          cp "$NFT_OUTPUT_FILE" "${NFT_OUTPUT_FILE}.backup"
      fi
  
      # Copier le nouveau fichier
      cp "$NFT_TMP" "$NFT_OUTPUT_FILE"

      # Recharger nftables
      log_info "Rechargement de nftables..."
  
      # D'abord supprimer la table si elle existe
      nft delete table inet whitelist-isp 2>/dev/null || true

      # Charger le nouveau fichier
      # Charger le nouveau fichier
      if nft -f "$NFT_OUTPUT_FILE"; then
          log_info "Whitelist AS mise à jour avec succès"
      else
          log_error "Échec du rechargement nftables"
          # Restaurer le backup
          if [ -f "${NFT_OUTPUT_FILE}.backup" ]; then
              cp "${NFT_OUTPUT_FILE}.backup" "$NFT_OUTPUT_FILE"
              nft delete table inet whitelist-isp 2>/dev/null || true
              nft -f "$NFT_OUTPUT_FILE" 2>/dev/null || true
          fi
          send_error_mail "Échec rechargement nftables sur $HOSTNAME" \
              "Le rechargement de nftables a échoué.\nLe backup a été restauré."
          exit 1
      fi
  fi
  
  log_info "=========================================="
  log_info "Fin de la mise à jour"
  log_info "=========================================="
  
  exit 0

Bon je te l’acorde c’est un peu une usine à gaz mais tu auras un serveur injoingnable par tout ce qui n’est pas explicitement authorisé (serveur coffre fort en qq sorte).

1 Like

Tout bloquer par défaut sauf exceptions et par géoip, oui je pourrai faire ça pour mon serveur PeerTube par exemple, où les gens ne comprennent pas ce que veut dire « francophones ».

Ce n’est pas vraiment usine à gaz si c’est bien écrit et les scripts sont là pour aider.
Le problème reste le même, il faut savoir quoi mettre en exception. Je ne pourrai pas faire ça avec Mastodon par exemple, un utilisateur francophone pourrait être sur une instance étrangère, de manière générale avec le Fediverse ce n’est pas possible.

1 Like

Pour le blocage par défaut, faut aussi garder en tête qu’il y a des publics francofones en Europe bien sûr (France, Belgique, Suisse), en Amérique (Canada, un chouïa dans quelques états des USA), et surtout en Afrique (environ la moitié des pays), continent avec le plus de locuteurs francofones au monde. Même si l’accès à Internet y est moins universel qu’en France, ça fait un paquet de monde !
+ Toutes les persones expatriées ou qui aprènent le français un peu partout sur le reste du globe, quoi ^^

Après je veus pas faire de leçon @greg, c’est sûr que ça doit doner beaucoup de tranquilité de n’autoriser que quelques AS :relieved:

Maintenant @carlos désolée je sors encore un peu du sujet, mais coment tu décides qu’un bloc d’IP est à jeter ? Ça m’intéresse vraiment.

Dans reaction il est par exemple possible de bloquer un /24 après que 10 de ses IPs aient été bloquées (via d’autres règles) en 2h.
Edit: je viens de documenter coment on fait ça dans reaction !

Mais je trouve pas ça très satisfaisant, déjà parce que c’est pas au courant de l’AS, parce que ça ne fait pas un décompte du nombre d’IPs, mais plutôt du nombre de blocages, et ensuite parce que je sais pas trop quèle règle générale on pourrait doner à tout ça.
J’aimerais ajouter cète compétence à reaction de manière un peu plus « built-in » que ça l’est actuèlement, mais je manque d’avis de sysadmins sur la question.

Les francophones (ou autres), aucun problème quelque soit l’endroit où ils se trouvent du moment qu’ils ne passent pas par des hébergeurs bloqués, les FAI (même étrangers) sont rarement bloqués, exception faite de certains pays d’Asie mais honnêtement je m’en fiche.

C’est exactement ce que fait mon script mais avec une petit + : il fait appel à une base de données d’ASN et si certains sont marqués « abuser » il les torpille. On peut retourner le problème dans tous les sens, je pense qu’il n’y a pas de bonnes ou de mauvaises pratiques, soit notre firewall nous protège efficacement voire nous surprotège avec quelques désagréments (qui nécessitent des ajustements), soit il est plus souple et avec moins de contraintes, mais avec un risque plus élevé de se faire pirater.

Moi je pense que…

1 Like

Carément :joy:
Et il est visible quelque part ce script ? :smirk:

Ya pas d’offense :smiley:
C’est réservé pour certains services comme un Dolibarr ou une gestion de fichier sensible, ça assure une sacré défense en profondeur.

1 Like

Tu donnes le service en question ? ça m’intéresse.

C’est un script perso et à destination de mon routeur Mikrotik mais ça doit s’adapter à d’autres marques. Flemme de commenter mais je peux le faire si tu le souhaites.

#!/bin/bash
IP=$1
NAME=$2
API_KEY="proutproutpouetpouet"
CACHE_DIR="/var/lib/defense_cache"
RESPONSE=$(curl -s "https://iplocate.io/api/lookup/$IP?apikey=$API_KEY")

if [ -z "$RESPONSE" ] || [[ "$RESPONSE" == *"error"* ]]; then
    logger "Script_Defense: Erreur API pour $IP, abandon."
    exit 1
fi

IS_VPN=$(echo "$RESPONSE" | jq -r '.privacy.is_vpn // false')
IS_ABUSER=$(echo "$RESPONSE" | jq -r '.privacy.is_abuser // false')
ASN_ROUTE=$(echo "$RESPONSE" | jq -r '.asn.route // "null"')
ASN_NAME=$(echo "$RESPONSE" | jq -r '.asn.name // "Unknown"')
COUNTRY_CODE=$(echo "$RESPONSE" | jq -r '.country_code // "XX"')

if [ "$ASN_ROUTE" != "null" ]; then
    CLUSTER_ID=$(echo "$ASN_ROUTE" | tr -d '/')
    CLUSTER_FILE="$CACHE_DIR/$CLUSTER_ID.history"
    mkdir -p "$CACHE_DIR"

    if ! grep -q "$IP" "$CLUSTER_FILE" 2>/dev/null; then
        echo "$IP" >> "$CLUSTER_FILE"
    fi

    COUNT=$(wc -l < "$CLUSTER_FILE")

    if [ "$COUNT" -ge 10 ]; then
        IS_ABUSER="true"
        logger "Script_Defense: Cluster détecté sur $ASN_ROUTE ($COUNT IP), bascule en CIDR forcé."

        logger "Script_Defense: Purge complète des IP du cluster $ASN_ROUTE dans F2B."

        DEL_CMD="/ip firewall address-list remove"
        [[ "$IP" == *":"* ]] && DEL_CMD="/ipv6 firewall address-list remove"

        while IFS= read -r OLD_IP; do
            ssh -i /root/.ssh/id_rsa -o StrictHostKeyChecking=no carlos@192.168.1.254 \
            "$DEL_CMD [find list=F2B address=$OLD_IP]" < /dev/null
        done < "$CLUSTER_FILE"
    fi
fi

if [ "$ASN_ROUTE" == "null" ]; then
    MASK=0
else
    MASK=$(echo "$ASN_ROUTE" | cut -d'/' -f2)
fi

if ([ "$IS_VPN" == "true" ] || [ "$IS_ABUSER" == "true" ]); then
    TARGET=$ASN_ROUTE
    LIST="CIDR"
    COMMENT="$ASN_NAME ($COUNTRY_CODE)"
else
    LIST="F2B"
    TARGET=$IP
    COMMENT="F2B-$NAME ($COUNTRY_CODE)"
fi

if [ "$LIST" == "CIDR" ]; then
    WIKI_PAGE="/var/www/dokuwiki/data/pages/securite/banlist.txt"
    NEW_LINE="| {{code>$TARGET}} | $ASN_NAME | $COUNTRY_CODE |"

    if ! grep -q "$TARGET" "$WIKI_PAGE"; then
        if [[ "$TARGET" == *":"* ]]; then
            sed -i "/===> FOOTER_START/i $NEW_LINE" "$WIKI_PAGE"
        else
            sed -i "/===> IPV6_START/i $NEW_LINE" "$WIKI_PAGE"
        fi
        logger "Script_Defense: Wiki mis à jour avec $TARGET"
    fi
fi

CMD="/ip firewall address-list add"
[[ "$IP" == *":"* ]] && CMD="/ipv6 firewall address-list add"

ssh -i /root/.ssh/id_rsa -o StrictHostKeyChecking=no carlos@192.168.1.254 "$CMD list=$LIST address=$TARGET comment=\"$COMMENT\""

logger "Script_Defense: Ajouté $TARGET à la liste $LIST ($COMMENT)"
1 Like

Super merci, come ça j’ai toutes les infos qui m’intéressent plutôt que de poser plein de petites questions !

Hello,

Je répond tardivement. Un peu à coté vu que ce n’est pas vraiment une solution technique.

Coté threadiverse (lemmy,piefed,mbin…) nous avons :

- un salon matrix qui permet de coordonnéer nos actions admins et purger les spams. Threadiverse defense et qui est seulement ouvert aux admins des instances du fediverse.

J’ignore si ce système existe pour mastodon.

  • Ensuite, ya le site I Challenge Thee où on peut endorser les instance du fédiverse bien modéré et de confiance.

Voilou, bonne journée

1 Like