Il y a 389 articles et 19 utilisateurs inscrits (ce bandeau est défini dans MediaWiki:Sitenotice)

Protection de Pleade contre les campagnes distribuées de robots

De AJLSM
Aller à la navigation Aller à la recherche

Protection de Pleade contre les campagnes distribuées de robots (modsecurity)

[modifier | modifier le wikicode]

Pour le contexte général et le principe des crawlers distribués, voir Protection contre les crawlers distribués de type « hyper-cluster ».

Notice :
Pour tous les serveurs hébergés par AJLSM, voir la documentation de protection utilisant haproxy : http://mw.ajlsm.com/wiki/Protection_des_recherches_Pleade_contre_les_campagnes_distribu%C3%A9es_de_robots_par_HAProxy.

Cette protection a été mise en place à la suite de campagnes de requêtes automatisées distribuées provoquant une saturation de Pleade et d'Apache. Le comportement observé était caractéristique d'une campagne coordonnée :

  • utilisation d'un grand nombre d'adresses IP différentes ;
  • changement possible de User-Agent ;
  • répétition du même tronc de recherche ;
  • variation systématique des facettes ;
  • requêtes individuellement peu nombreuses par adresse IP, rendant inefficace un simple filtrage par IP ;
  • génération de traitements Pleade coûteux pouvant provoquer des délais d'attente de 30 secondes côté Drupal puis des erreurs HTTP 500.

Un exemple observé était la répétition de :

query1=Mont Athos

depuis un grand nombre d'adresses IP, avec différentes combinaisons de facettes.

La protection ne cherche donc pas à identifier le robot par son adresse IP ou son User-Agent. Elle analyse le comportement de recherche.


Principe général

[modifier | modifier le wikicode]

La protection s'applique à toutes les pages publiques Pleade situées sous /archives-en-ligne/ dont le nom se termine par results.html.

Exemples :

/archives-en-ligne/results.html
/archives-en-ligne/federate-results.html
/archives-en-ligne/cdc-results.html
/archives-en-ligne/tab-results.html
/archives-en-ligne/mosaic-results.html

La règle est volontairement générique afin de couvrir également d'autres modes de résultats utilisant le même schéma de nommage.

La protection fonctionne à plusieurs niveaux :

  1. blocage immédiat des recherches demandant un nombre excessif de résultats (`hpp > 1000`) ;
  2. blocage immédiat des recherches comportant au moins 6 facettes ;
  3. détection par ModSecurity des campagnes distribuées utilisant le même tronc de recherche ;
  4. transmission de la décision de ModSecurity à Apache ;
  5. retour HTTP 429 avant que la requête n'atteigne Drupal/Pleade/Tomcat.

Le code HTTP utilisé est :

429 Too Many Requests

Une page explicative est présentée à l'utilisateur.


Le trafic suit approximativement le chemin :

Internet
   |
   v
proxy / frontal HTTPS
   |
   v
Apache
   |
   +--> ModSecurity
   |       |
   |       +--> analyse du comportement
   |
   +--> mod_rewrite
   |       |
   |       +--> HTTP 429 si nécessaire
   |
   v
Drupal
   |
   v
Pleade / Tomcat

L'objectif essentiel est d'arrêter les requêtes anormales avant Drupal et Pleade.


Le serveur utilise ModSecurity 2.9.x.

Le moteur général reste en :

SecRuleEngine DetectionOnly

Les règles spécifiques à Pleade utilisent donc ModSecurity pour détecter le comportement et positionner une variable d'environnement.

Le blocage HTTP proprement dit est effectué ensuite par Apache/mod_rewrite.

Cela évite d'activer globalement les actions disruptives de ModSecurity et des règles OWASP CRS.


Détection d'un même tronc de recherche

[modifier | modifier le wikicode]

Le fichier spécifique est :

/etc/modsecurity/pleade-cluster-detection.conf

Le principe consiste à utiliser la valeur de `query1` comme clé d'une collection persistante `GLOBAL`.

Ainsi :

IP A -> query1=Mont Athos
IP B -> query1=Mont Athos
IP C -> query1=Mont Athos
IP D -> query1=Mont Athos

alimentent tous le même compteur.

Cela permet de détecter une campagne distribuée même lorsque chaque adresse IP ne fait qu'une seule requête.

La persistance des collections ModSecurity permet à plusieurs requêtes et processus Apache de partager ce compteur.


Prise en compte des facettes

[modifier | modifier le wikicode]

Une recherche n'alimente le compteur que si elle comporte au moins deux paramètres de type :

fchampN=

Par exemple :

fchamp1=fpersname
fchamp2=fgeogname

Cette condition réduit le risque qu'une recherche simple et légitime très populaire soit considérée comme une campagne automatisée.


Gestion temporelle du compteur

[modifier | modifier le wikicode]

Première version : expirevar

[modifier | modifier le wikicode]

La première version utilisait :

expirevar:global.pleade_hits=120

Cette solution fonctionnait mais ne constituait pas réellement une fenêtre de 120 secondes.

À chaque nouvelle requête, l'expiration était repoussée. Une campagne continue pouvait donc produire :

hits=100
hits=200
hits=300
hits=423
...

et le compteur ne disparaissait que 120 secondes après l'arrêt des requêtes.

Ce comportement est efficace pour bloquer une attaque continue, mais le compteur peut augmenter indéfiniment tant que l'activité se poursuit.


Version avec décroissance temporelle : deprecatevar

[modifier | modifier le wikicode]

Pour éviter cette accumulation, le compteur peut utiliser `deprecatevar`.

Exemple de principe :

setvar:global.pleade_hits=+1,
deprecatevar:global.pleade_hits=10/120

`deprecatevar` fait décroître une variable persistante au fil du temps.

Il ne s'agit pas d'une fenêtre glissante stricte au sens :

nombre exact de requêtes reçues pendant les 120 dernières secondes

mais d'un score temporel décroissant.

Le principe devient :

requêtes rapprochées
      |
      v
le score augmente
      |
      v
seuil atteint
      |
      v
HTTP 429

puis, lorsque l'activité diminue :

score
  |
  v
décroissance avec le temps
  |
  v
retour sous le seuil
  |
  v
recherches de nouveau autorisées

Cela correspond mieux au besoin qu'un compteur simplement maintenu en vie par `expirevar`.

Important : la valeur de décroissance doit être réglée après observation du trafic réel.

Une valeur telle que :

deprecatevar:global.pleade_hits=10/120

constitue un paramètre de départ à valider et non une constante intrinsèque à Pleade.

`deprecatevar` doit également être exécuté régulièrement pour que la décroissance soit appliquée.


Règle ModSecurity

[modifier | modifier le wikicode]

Le fichier :

/etc/modsecurity/pleade-cluster-detection.conf

peut utiliser le principe suivant :

#
# Détection des campagnes distribuées Pleade
#

# 1 - Comptage
#
# Même query1 + au moins 2 facettes.
# Toutes les IP alimentent le même compteur associé à query1.

SecRule REQUEST_URI \
"^/archives-en-ligne/(?:[A-Za-z0-9_-]+-)?results\.html(?:\?|$)" \
"id:1100001,phase:1,pass,nolog,chain"
    SecRule ARGS_GET:query1 "!^$" \
        "initcol:global=%{ARGS_GET.query1},chain"
    SecRule &ARGS_GET:/^fchamp[0-9]+$/ "@ge 2" \
        "setvar:global.pleade_hits=+1,\
         deprecatevar:global.pleade_hits=10/120"

# 2 - Détection
#
# Le seuil atteint provoque la création de la variable
# d'environnement utilisée ensuite par mod_rewrite.

SecRule GLOBAL:pleade_hits "@ge 10" \
"id:1100002,phase:1,pass,log,auditlog,\
msg:'PLEADE_CLUSTER distributed search detected',\
logdata:'query1=%{ARGS_GET.query1} hits=%{GLOBAL.pleade_hits} ip=%{REMOTE_ADDR}',\
setenv:PLEADE_CLUSTER_BLOCK=1"

Attention : la variante utilisant `deprecatevar` doit être testée sur le serveur avant de remplacer définitivement la configuration utilisant `expirevar`.

La version `expirevar` a été validée en production ; la version `deprecatevar` constitue l'évolution destinée à introduire la décroissance temporelle.

Création de la page HTML 429

[modifier | modifier le wikicode]

Exemple de page d'erreur

[modifier | modifier le wikicode]

Il faut créer une en-tête qui rappelle à minima le contexte graphique du site. dans l'exemple suivant, le client est Pandor MSH-Dijon :

Il faut personnaliser :

  • le title,
  • l'en-tête,
  • le nom du site qui est donné dans les paragraphes de présentation,
  • l'adresse courriel.

Les liens vers les medias et css dans Drupal sont donnés à titre d'exemple. Il faut vérifier s'ils sont bien réolus.

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <title>Requête bloquée | '''PANDOR'''</title>

  <style>
    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      font-family: Arial, Helvetica, sans-serif;
    }

    /* ===== Entête eArchives 64 ===== */
    [...]
 </style>
</head>

<body>

<header class="pandor-header">

  <div class="pandor-header-main">

    <div class="pandor-logo">
      <a href="/fr" title="Accueil PANDOR">
        <img
          src="/sites/default/files/images/icones/logo-pandor_300.png"
          alt="PANDOR">
      </a>
    </div>

    <div class="pandor-content">

      <div class="pandor-slogan">
        Bienvenue sur le portail archives numériques et données de la recherche
      </div>

      <div class="pandor-tools-row">

        <div class="pandor-search" aria-hidden="true">
          <span class="pandor-search-text">Texte libre</span>
        </div>

        <a class="pandor-advanced"
           href="/fr/archives-en-ligne/default-search-form.html">
          Recherche avancée
        </a>

        <div class="pandor-icons">

          <a href="/authentification-archives-en-ligne"
             title="S'authentifier">
            <img src="/sites/default/files/images/icones/user_66.png"
                 alt="Utilisateur">
          </a>

          <a href="/archives-en-ligne/historic.html"
             title="Voir l'historique">
            <img src="/sites/default/files/images/icones/history_66.png"
                 alt="Historique">
          </a>

          <a href="/archives-en-ligne/basket.html"
             title="Voir le porte-document">
            <img src="/sites/default/files/images/icones/basquet_66.png"
                 alt="Panier">
          </a>

          <a href="/aide"
             title="Voir l'aide">
            <img src="/sites/default/files/images/icones/help_66.png"
                 alt="Aide">
          </a>

        </div>

      </div>
    </div>

  </div>

  <nav class="pandor-nav" aria-label="Navigation principale">
    <div class="pandor-nav-inner">

      <a href="/fr">Accueil</a>

      <a href="/fr/qui-sommes-nous">
        Qui sommes-nous ?
      </a>

      <a href="/fr/fonds">
        Fonds
      </a>

      <a href="/fr/node/3">
        Programmes de recherche
      </a>

      <a href="/fr/actualites">
        Actualités
      </a>

    </div>
  </nav>

</header>
<main class="blocked-search-wrapper">

  <div class="blocked-search-box">

    <h1>Requête bloquée</h1>

    <p>
      Cette requête a été bloquée automatiquement car elle présente
      un volume ou une combinaison de critères de recherche inhabituels.
    </p>

    <p>
      Cette protection vise à empêcher les robots, systèmes automatisés
      et outils d'intelligence artificielle de générer des requêtes
      susceptibles de surcharger le service.
    </p>

    <p>
      Si vous êtes un utilisateur du site et pensez que ce blocage
      n'est pas normal, vous pouvez nous le signaler.
    </p>

    <div class="blocked-search-actions">

      <a href="/fr">
        Retour à l'accueil
      </a>

      <a id="contact-requete-bloquee"
         href="#">
        Signaler ce blocage
      </a>

    </div>

    <div class="blocked-search-url">
      <strong>Requête concernée :</strong><br>
      <span id="blocked-url"></span>
    </div>

  </div>

</main>

<script>
document.addEventListener('DOMContentLoaded', function () {

  const lien = document.getElementById('contact-requete-bloquee');
  const zoneUrl = document.getElementById('blocked-url');

  const urlBloquee = window.location.href;

  zoneUrl.textContent = urlBloquee;

  const sujet =
    'Signalement d’une recherche bloquée sur PANDOR';

  const corps =
    'Bonjour,\n\n' +
    'Ma recherche sur PANDOR a été bloquée automatiquement.\n\n' +
    'Requête concernée :\n' +
    urlBloquee + '\n\n' +
    'Informations complémentaires :\n\n';

  /*
   * Adresse à renseigner avant mise en production.
   */
  lien.href =
    'mailto:pandor@u-bourgogne.fr' +
    '?subject=' + encodeURIComponent(sujet) +
    '&body=' + encodeURIComponent(corps);
});
</script>

</body>
</html>

Droits sur la page d'erreur

[modifier | modifier le wikicode]

Le répertoire contenant la page d'erreur doit être traversable par l'utilisateur Apache (www-data).

Appliquer les droits suivants :

chmod 755 /var/www/apache-errors
chmod 644 /var/www/apache-errors/requete-trop-complexe.html

Vérifier les droits sur l'ensemble du chemin :

namei -l /var/www/apache-errors/requete-trop-complexe.html

On doit notamment obtenir :

drwxr-xr-x root root apache-errors
-rw-r--r-- root root requete-trop-complexe.html

Le bit x du répertoire est indispensable : sans lui, Apache peut disposer des autorisations nécessaires dans sa configuration (Require all granted) tout en retournant néanmoins une erreur 403 Forbidden, car l'utilisateur www-data ne peut pas traverser le répertoire.

Vérification de la page d'erreur

[modifier | modifier le wikicode]

Avant de tester les règles de blocage, vérifier que la page d'erreur est directement accessible.

Exemple lorsque le virtualhost HTTPS est accessible localement :

curl -k -I \
  --resolve 'earchives.le64.fr:443:127.0.0.1' \
  'https://earchives.le64.fr/__ajlsm-errors/requete-trop-complexe.html'

Résultat attendu :

HTTP/1.1 200 OK

Blocage Apache

[modifier | modifier le wikicode]

Le fichier Apache est :

/etc/apache2/conf-available/block-complex-searches.conf

Lorsque le seuil est atteint, ModSecurity positionne :

PLEADE_CLUSTER_BLOCK=1

Apache applique alors :

# Blocage générique d'une campagne distribuée détectée par ModSecurity
RewriteCond %{ENV:PLEADE_CLUSTER_BLOCK} =1
RewriteRule ^ - [R=429,L]

La requête est ainsi arrêtée avant Drupal/Pleade.


Blocage de hpp

[modifier | modifier le wikicode]

Les recherches demandant plus de 1000 résultats sont bloquées :

# Bloquer les recherches Pleade avec plus de 1000 résultats demandés
#
# S'applique à toutes les pages publiques se terminant par results.html :
# results.html, federate-results.html, cdc-results.html,
# tab-results.html, mosaic-results.html, etc.
RewriteCond %{REQUEST_URI} ^/archives-en-ligne/(?:[A-Za-z0-9_-]+-)?results\.html$ [NC]
RewriteCond %{QUERY_STRING} "(^|&)hpp=(?:100[1-9]|10[1-9][0-9]|1[1-9][0-9]{2}|[2-9][0-9]{3}|[1-9][0-9]{4,})(?:&|$)" [NC]
RewriteRule ^ - [R=429,L]

Ainsi :

hpp=1000     -> autorisé
hpp=1001     -> HTTP 429
hpp=10000    -> HTTP 429
hpp=1000000  -> HTTP 429

Blocage des recherches comportant au moins 6 facettes

[modifier | modifier le wikicode]

Indépendamment du mécanisme de détection distribuée, une requête comportant au moins 6 `fchampN` est immédiatement bloquée :

# 2. Bloquer les recherches avec au moins 6 facettes
#
# S'applique à toutes les pages publiques se terminant par results.html :
# results.html, federate-results.html, cdc-results.html,
# tab-results.html, mosaic-results.html, etc.
RewriteCond %{REQUEST_URI} ^/archives-en-ligne/(?:[A-Za-z0-9_-]+-)?results\.html$ [NC]
RewriteCond %{QUERY_STRING} "(?:^|&)fchamp[0-9]+=[^&]*(?:&[^&]*)*(?:&fchamp[0-9]+=[^&]*(?:&[^&]*)*){5}" [NC]
RewriteRule ^ - [R=429,L]

La numérotation des `fchamp` n'a pas besoin d'être continue.

La page statique est stockée dans :

/var/www/apache-errors/requete-trop-complexe.html

Configuration Apache :

Alias /__ajlsm-errors/ /var/www/apache-errors/

<Directory "/var/www/apache-errors">
    Require all granted
</Directory>

ErrorDocument 429 /__ajlsm-errors/requete-trop-complexe.html

Une exception d'autorisation est également nécessaire afin que cette page puisse être servie même lorsqu'un robot est déjà interdit :

<Location "/__ajlsm-errors/">
    Require all granted
</Location>

Activation de la configuration apache

[modifier | modifier le wikicode]
a2enconf block-complex-searches

Validation de la configuration

[modifier | modifier le wikicode]

Après toute modification :

apache2ctl configtest

Le résultat attendu est :

Syntax OK

Puis :

systemctl reload apache2

Ne jamais recharger Apache si `configtest` retourne une erreur.

Test du mécanisme distribué

[modifier | modifier le wikicode]

Un test contrôlé peut être réalisé avec :

for i in $(seq 1 12); do
  echo -n "$i -> "
  curl -s -o /dev/null -w '%{http_code}\n' \
    -H 'Host: salamandre.college-de-france.fr' \
    "http://127.0.0.1/archives-en-ligne/federate-results.html?query1=TESTBLOCK&fchamp1=a&fquery1=x&fchamp2=b&fquery2=$i"
done

Avec un seuil à 10, le comportement obtenu lors de la validation était :

1 -> 200
2 -> 200
3 -> 200
4 -> 200
5 -> 200
6 -> 200
7 -> 200
8 -> 200
9 -> 200
10 -> 429
11 -> 429
12 -> 429


Test de la décroissance temporelle

[modifier | modifier le wikicode]

Après mise en place de `deprecatevar`, il faut vérifier son comportement avant de considérer la configuration comme validée.

Le test doit permettre :

  1. de faire monter le compteur au-dessus du seuil ;
  2. de vérifier le retour HTTP 429 ;
  3. de cesser les requêtes pendant la durée choisie ;
  4. d'envoyer ensuite une nouvelle requête ;
  5. de vérifier que le score a effectivement diminué ;
  6. de vérifier que la recherche redevient autorisée lorsque le score repasse sous le seuil.

Le compteur peut être observé avec :

grep -a 'PLEADE_CLUSTER distributed search detected' \
  /var/log/apache2/modsec_audit.log | tail -50

Ce test doit être réalisé avant de remplacer définitivement la configuration `expirevar` actuellement validée.


File d'attente Apache

[modifier | modifier le wikicode]
watch -n 1 "ss -lnt | grep ':80 '"

Campagnes détectées

[modifier | modifier le wikicode]
grep -a 'PLEADE_CLUSTER distributed search detected' \
  /var/log/apache2/modsec_audit.log | tail -50

Recherche particulière

[modifier | modifier le wikicode]

Par exemple :

grep -a 'PLEADE_CLUSTER distributed search detected' \
  /var/log/apache2/modsec_audit.log \
  | grep 'Mont Athos' \
  | tail -50


Résultat observé le 23 septembre 2026

[modifier | modifier le wikicode]

Lors de l'incident, le même tronc :

query1=Mont Athos

a été envoyé successivement depuis un grand nombre d'adresses IP différentes.

Le compteur partagé a dépassé :

hits=400

alors que les adresses IP changeaient pratiquement à chaque requête.

Exemple de principe observé :

query1=Mont Athos hits=414 ip=157.254.115.104
query1=Mont Athos hits=415 ip=209.139.24.95
query1=Mont Athos hits=416 ip=206.204.19.28
query1=Mont Athos hits=417 ip=170.0.72.172
query1=Mont Athos hits=418 ip=200.151.163.159
...

Cela démontre l'intérêt de la corrélation sur le contenu de la recherche plutôt que sur l'adresse IP.

La mise en place du blocage en amont de Drupal/Pleade permet d'éviter que chaque requête automatisée déclenche un traitement applicatif coûteux.


Requête
   |
   +-- hpp > 1000 ------------------------> HTTP 429
   |
   +-- >= 6 facettes ---------------------> HTTP 429
   |
   +-- même query1 + >= 2 facettes
            |
            v
       compteur GLOBAL
            |
            +-- activité faible
            |       |
            |       +--> décroissance temporelle
            |
            +-- activité importante
                    |
                    +--> seuil >= 10
                              |
                              v
                     PLEADE_CLUSTER_BLOCK=1
                              |
                              v
                          HTTP 429

La protection ne dépend donc pas :

  • de l'adresse IP ;
  • du réseau d'origine ;
  • du User-Agent ;
  • du navigateur déclaré.

Elle repose principalement sur le comportement collectif des requêtes.

Cette architecture permet de traiter aussi bien une requête individuellement excessive qu'une campagne distribuée utilisant un grand nombre d'adresses IP différentes.

Protection spécifique contre les robots Meta

[modifier | modifier le wikicode]

Les robots Meta connus sont également bloqués indépendamment du mécanisme comportemental :

Deux méthodes fonctionnent :

Dans la configuration apache du site, et juste après :

RewriteEngine On
RewriteOptions inherit

Ajouter :

# Blocage des crawlers Meta / Facebook
RewriteCond %{HTTP_USER_AGENT} "(meta-webindexer|meta-externalagent|meta-externalfetcher|facebookexternalhit|facebot|facebookbot)" [NC]
RewriteRule ^ - [F,L]
SetEnvIfNoCase User-Agent "meta-webindexer" meta_bot
SetEnvIfNoCase User-Agent "meta-externalagent" meta_bot
SetEnvIfNoCase User-Agent "meta-externalfetcher" meta_bot
SetEnvIfNoCase User-Agent "facebookexternalhit" meta_bot
SetEnvIfNoCase User-Agent "Facebot" meta_bot
<Location "/">
    <RequireAll>
        Require all granted
        Require not env meta_bot
    </RequireAll>
</Location>

Validation et vérification

[modifier | modifier le wikicode]

Puis :

/usr/sbin/apache2ctl configtest
systemctl reload apache2

Et test, par exemple pour le site pandor2026-preprod.ube.fr :

for ua in \
  meta-webindexer \
  meta-externalagent \
  meta-externalfetcher \
  facebookexternalhit \
  facebot \
  facebookbot
do
  printf '%-25s -> ' "$ua"
  curl -s -o /dev/null -w '%{http_code}\n' \
    -H 'Host: pandor2026-preprod.ube.fr' \
    -A "$ua" \
    http://127.0.0.1/
done

Attendu : 403 partout.

Puis contrôle négatif :

curl -s -o /dev/null -w '%{http_code}\n' \
  -H 'Host: pandor2026-preprod.ube.fr' \
  -A 'Mozilla/5.0' \
  http://127.0.0.1/

Ce filtrage constitue une protection supplémentaire mais ne doit pas être considéré comme suffisant : un robot peut modifier son User-Agent.

Protection de Pleade contre les campagnes de robots et le moissonnage IIIF

[modifier | modifier le wikicode]

Cette documentation décrit les mécanismes de protection mis en place autour de Pleade contre :

  • les campagnes distribuées de recherches automatisées ;
  • les recherches volontairement excessives ;
  • les robots connus, notamment certains robots Meta/Facebook ;
  • le moissonnage massif des métadonnées IIIF ;
  • le téléchargement automatisé d'images IIIF complètes en pleine résolution.

Les mécanismes utilisés dépendent de l'architecture d'hébergement.

Choix de la protection selon l'architecture

[modifier | modifier le wikicode]

Serveurs hébergés par AJLSM

[modifier | modifier le wikicode]

Pour les serveurs hébergés par AJLSM et placés derrière les deux HAProxy, les protections doivent être mises en œuvre en priorité au niveau HAProxy.

Cela permet :

  • de centraliser les règles ;
  • d'appliquer une protection identique à plusieurs clients ;
  • d'arrêter les requêtes avant Apache, Drupal, Pleade et Tomcat ;
  • de partager les compteurs entre les deux HAProxy ;
  • d'éviter la multiplication de règles Apache spécifiques à chaque site.

Voir également :

Protection des recherches Pleade contre les campagnes distribuées de robots par HAProxy

La protection IIIF décrite plus bas dans cette page complète cette protection des recherches.

Serveurs ne passant pas par les HAProxy AJLSM

[modifier | modifier le wikicode]

Pour les installations Pleade hébergées à l'extérieur de l'infrastructure AJLSM, la protection peut être réalisée localement avec :

  • Apache ;
  • ModSecurity ;
  • mod_rewrite.

Les chapitres consacrés à ModSecurity et Apache restent donc nécessaires pour ces installations.


1. Protection HAProxy des sites hébergés par AJLSM

[modifier | modifier le wikicode]

1.1. Architecture

[modifier | modifier le wikicode]

Pour les sites hébergés par AJLSM, le trafic suit le chemin :

Internet
   |
   v
HAProxy-01 / HAProxy-02
   |
   |-- protections recherches Pleade
   |-- protections IIIF
   |-- limitation comportementale
   |
   v
Apache
   |
   v
Drupal
   |
   v
Pleade / Tomcat

L'objectif est d'arrêter les requêtes manifestement anormales le plus en amont possible.

Une requête rejetée par HAProxy n'atteint donc ni Apache, ni Drupal, ni Pleade/Tomcat.

1.2. Emplacement de la configuration

[modifier | modifier le wikicode]

La configuration HAProxy est gérée par Ansible.

Le fichier principal contenant les règles spécifiques AJLSM est :

inventory/group_vars/ajlsm_loadbalancer/haproxy.yml

La configuration est injectée dans HAProxy par le rôle :

roles/haproxy/

Le template principal est :

roles/haproxy/templates/haproxy.cfg.j2

Il contient notamment :

{{ haproxy_custom_configuration }}

Il ne faut donc pas ajouter directement les protections Pleade dans le template générique : elles doivent être définies dans :

inventory/group_vars/ajlsm_loadbalancer/haproxy.yml

1.3. Réplication des compteurs entre HAProxy

[modifier | modifier le wikicode]

Les deux HAProxy utilisent le mécanisme de peers :

peers pleade_peers
    peer haproxy-01 10.0.0.30:10000
    peer haproxy-02 10.0.0.31:10000

Cela permet notamment de partager les stick-tables entre :

  • HAProxy-01 ;
  • HAProxy-02.

Une adresse IP ne peut donc pas contourner simplement une limitation en passant alternativement par les deux HAProxy.


2. Protection contre le moissonnage IIIF

[modifier | modifier le wikicode]

2.1. Incident ayant conduit à la protection

[modifier | modifier le wikicode]

Le 7 et le 8 octobre 2026, une activité anormale a été observée sur le site des Archives départementales de la Mayenne.

Une adresse IP effectuait un nombre extrêmement important de requêtes IIIF sur :

manifest.json
info.json

L'adresse observée était notamment :

54.36.103.227

Le comportement ne correspondait pas à une navigation humaine normale.

Sur une période de deux heures, environ :

24 115 requêtes

ont été observées.

La très grande majorité concernait les métadonnées IIIF, notamment :

/pleade/iiif/.../info.json
/archives-en-ligne/iiif/.../info.json

L'adresse IP utilisait par ailleurs un User-Agent ressemblant à celui d'un navigateur Chrome normal.

Il n'est donc pas pertinent de baser cette protection sur le User-Agent.

2.2. Deux chemins IIIF à prendre en compte

[modifier | modifier le wikicode]

Pleade/Drupal utilise deux formes d'URL pour accéder aux informations IIIF :

/pleade/iiif/...
/archives-en-ligne/iiif/...

Une navigation humaine peut provoquer des appels sur les deux chemins pour une même vue.

Par exemple :

/pleade/iiif/ark:/37963/.../f20/info.json
/archives-en-ligne/iiif/ark:/37963/.../f20/info.json

La protection doit donc impérativement prendre en compte les deux chemins.

2.3. Pourquoi ne pas compter les images

[modifier | modifier le wikicode]

Une première version de la protection utilisait deux compteurs :

gpc0 = nombre de requêtes manifest.json / info.json
gpc1 = nombre de requêtes d'images

Le principe envisagé était de bloquer une adresse IP effectuant beaucoup de requêtes de métadonnées tout en demandant très peu d'images.

Les observations en production ont montré que cette méthode n'était pas fiable.

Une navigation humaine normale pouvait produire de nombreux :

info.json
manifest.json

sans qu'une requête d'image correspondant aux ACL initialement prévues soit visible au niveau HAProxy.

Les images peuvent notamment être servies selon d'autres mécanismes, et la présence ou l'absence d'une requête :

*.jpg

ne constitue donc pas un indicateur suffisamment fiable du caractère humain ou automatisé de la consultation.

Le compteur d'images `gpc1` a donc été supprimé.

La détection repose désormais uniquement sur la cadence des requêtes de métadonnées IIIF.


3. Stick-table IIIF

[modifier | modifier le wikicode]

Une stick-table spécifique est utilisée.

Elle est indépendante de la stick-table déjà utilisée dans le frontend pour la protection des recherches Pleade.

Configuration :

backend iiif_abuse
    stick-table type ip size 200k expire 30m store gpc0,gpc0_rate(10m) peers pleade_peers

La clé est l'adresse IP :

type ip

La table peut contenir :

200 000 entrées

Une entrée inactive expire après :

30 minutes

Le compteur utilisé est :

gpc0

et son taux est calculé sur :

10 minutes

grâce à :

gpc0_rate(10m)

La table est synchronisée entre les deux HAProxy grâce à :

peers pleade_peers

4. ACL des métadonnées IIIF

[modifier | modifier le wikicode]

L'ACL couvre :

  • `manifest.json` ;
  • `info.json` ;
  • le chemin `/pleade/iiif/` ;
  • le chemin `/archives-en-ligne/iiif/`.

Configuration :

acl iiif_metadata path_reg -i ^/(pleade|archives-en-ligne)/iiif/.*(/manifest\.json|/info\.json)$

Le flag :

-i

rend la comparaison insensible à la casse.


5. Utilisation de sc1

[modifier | modifier le wikicode]

La protection des recherches Pleade utilise déjà :

sc0

Le suivi IIIF utilise donc :

sc1

Configuration :

http-request track-sc1 src table iiif_abuse if iiif_metadata

Chaque requête de métadonnées IIIF incrémente ensuite :

http-request sc-inc-gpc0(1) if iiif_metadata

6. Seuil de blocage IIIF

[modifier | modifier le wikicode]

Le seuil initial retenu après observation du trafic réel est :

400 requêtes de métadonnées IIIF / 10 minutes / adresse IP

Configuration :

acl iiif_metadata_abuse sc_gpc0_rate(1) gt 400

Lorsque le seuil est dépassé, HAProxy retourne :

HTTP 429 Too Many Requests

avec la page personnalisée AJLSM :

http-request return status 429 content-type "text/html; charset=utf-8" lf-file /etc/haproxy/errors/429.html if iiif_metadata iiif_metadata_abuse

Important : cette directive doit rester sur une seule ligne dans la configuration HAProxy.

La forme suivante a été testée et provoque une erreur de validation :

http-request return status 429 \
    content-type "text/html; charset=utf-8" \
    lf-file /etc/haproxy/errors/429.html \
    if iiif_metadata iiif_metadata_abuse

HAProxy 2.8 interprète ici le caractère `\` comme un élément de la directive et retourne notamment :

'http-request return' expects either 'if' or 'unless'
followed by a condition but found '\'

La validation Ansible empêche heureusement cette configuration incorrecte d'être déployée.


7. Justification du seuil de 400

[modifier | modifier le wikicode]

Le seuil a été déterminé après observation du trafic réel.

Des utilisateurs légitimes actifs ont été observés avec des valeurs nettement inférieures.

Par exemple :

~ 80 à 100 requêtes / 10 min

ont été observées lors de navigations humaines soutenues.

À l'inverse, le robot observé sur :

54.36.103.227

avait produit environ :

24 115 requêtes / 2 heures

soit, pendant son activité, un ordre de grandeur très supérieur à celui d'un utilisateur humain.

Le seuil :

400 / 10 min

constitue donc volontairement une marge importante par rapport aux usages humains observés.

Ce seuil pourra être réévalué après observation du trafic sur une période plus longue.


8. Surveillance de la stick-table

[modifier | modifier le wikicode]

La table peut être interrogée directement sur HAProxy-01 ou HAProxy-02.

Commande brute :

echo "show table iiif_abuse" | \
sudo socat stdio /var/run/haproxy/admin.sock

Exemple :

# table: iiif_abuse, type: ip, size:204800, used:18
0x...: key=109.208.12.191 use=0 exp=... gpc0=89 gpc0_rate(600000)=89
0x...: key=148.230.46.38 use=0 exp=... gpc0=93 gpc0_rate(600000)=93

`600000` correspond à :

600 000 ms = 600 secondes = 10 minutes

Affichage lisible et trié

[modifier | modifier le wikicode]
echo "show table iiif_abuse" | \
sudo socat stdio /var/run/haproxy/admin.sock | \
awk '
/key=/ {
    ip=$2
    sub("key=","",ip)

    for (i=1;i<=NF;i++) {
        if ($i ~ /^gpc0_rate/) {
            split($i,a,"=")
            meta=a[2]
        }
    }

    printf "%6d  %s\n",meta,ip
}' |
sort -nr

Exemple de résultat :

    93  148.230.46.38
    89  109.208.12.191
    33  128.79.166.14
    24  90.16.79.205

Surveillance d'une adresse particulière

[modifier | modifier le wikicode]

À exécuter sur un serveur HAProxy :

watch -n 5 '
echo "show table iiif_abuse" |
sudo socat stdio /var/run/haproxy/admin.sock |
grep "key=148.230.46.38"
'

Attention : le socket :

/var/run/haproxy/admin.sock

est présent sur les serveurs HAProxy.

Il n'est pas disponible sur un serveur applicatif tel que AD53.


Validation du compteur IIIF

[modifier | modifier le wikicode]

Le mécanisme de limitation a été validé le 8 octobre 2026 avec un test contrôlé envoyé depuis la VM AD53 vers la VIP HAProxy.

La requête de test devait impérativement utiliser le bon nom DNS dans le SNI TLS. Le frontend HAProxy utilise en effet :

bind 10.0.0.32:443 ssl crt /etc/haproxy/certs/ strict-sni

Un appel direct vers :

https://10.0.0.32/

même accompagné d'un en-tête :

Host: archives.lamayenne.fr

ne suffit pas, car le SNI TLS reste alors `10.0.0.32`.

Le test correct utilise donc `--resolve` :

curl -k -I \
  --resolve archives.lamayenne.fr:443:10.0.0.32 \
  'https://archives.lamayenne.fr/pleade/iiif/ark:/37963/r688343z99mnqk/f63/info.json'

Résultat attendu avant dépassement du seuil :

HTTP/2 200

La première requête a créé dans la stick-table l'entrée correspondant à l'adresse IP interne d'AD53 :

key=10.0.0.26

Le test de charge contrôlé a ensuite été effectué avec :

for i in $(seq 1 405); do
  code=$(curl -k -s -o /dev/null -w '%{http_code}' \
    --resolve archives.lamayenne.fr:443:10.0.0.32 \
    'https://archives.lamayenne.fr/pleade/iiif/ark:/37963/r688343z99mnqk/f63/info.json')

  printf "%3d -> %s\n" "$i" "$code"
done

La règle HAProxy étant :

acl iiif_metadata_abuse sc_gpc0_rate(1) gt 400

le comportement attendu est :

gpc0_rate <= 400  -> requête autorisée
gpc0_rate > 400   -> HTTP 429

Lors du test, une requête avait déjà été envoyée avant la boucle.

La 400e requête de la boucle a donc porté le compteur réel à 401 et a provoqué le premier blocage :

391 -> 200
392 -> 200
393 -> 200
394 -> 200
395 -> 200
396 -> 200
397 -> 200
398 -> 200
399 -> 200
400 -> 429
401 -> 429
402 -> 429
403 -> 429
404 -> 429
405 -> 429

La stick-table confirmait alors :

key=10.0.0.26 ... gpc0=406 gpc0_rate(600000)=406

Le test valide donc les points suivants :

  • les requêtes `info.json` sont bien détectées ;
  • le suivi est effectué par adresse IP ;
  • le compteur sur 10 minutes fonctionne ;
  • le seuil de 400 est respecté ;
  • la 401e requête présente dans la fenêtre déclenche bien un HTTP 429 ;
  • les requêtes suivantes continuent à incrémenter le compteur même lorsqu'elles sont rejetées ;
  • un robot qui insiste reste donc au-dessus du seuil tant que son activité continue ;
  • le blocage est réalisé directement par HAProxy, avant Apache, Drupal, Pleade et Tomcat.

L'entrée de test peut être laissée expirer naturellement.

Avec :

expire 30m

l'entrée disparaît après 30 minutes d'inactivité.

Le taux :

gpc0_rate(600000)

diminue de son côté au fur et à mesure que les requêtes quittent la fenêtre glissante de 10 minutes.

9. Vérification de la synchronisation HAProxy

[modifier | modifier le wikicode]

La même commande peut être exécutée successivement sur :

haproxy-01
haproxy-02

Les valeurs doivent être très proches.

De petites différences sont normales lorsque les commandes ne sont pas exécutées exactement au même instant.

Lors des tests du 8 octobre 2026, les mêmes adresses et pratiquement les mêmes compteurs étaient présents sur les deux nœuds.

Cela valide le fonctionnement de :

peers pleade_peers

10. Blocage des téléchargements IIIF pleine résolution

[modifier | modifier le wikicode]

10.1. Comportement observé

[modifier | modifier le wikicode]

Un autre comportement automatisé a été observé indépendamment du moissonnage de `info.json`.

Il consistait à demander systématiquement des images IIIF complètes sous la forme :

/full/pct:100/0/default.jpg

Ce type de requête demande l'image complète à 100 %.

Un robot effectuant massivement ce type de requête peut provoquer :

  • des lectures importantes sur le stockage ;
  • des transferts réseau importants ;
  • des traitements d'images coûteux ;
  • une charge inutile sur IIPImage et l'infrastructure.

10.2. Première protection Apache

[modifier | modifier le wikicode]

La protection avait initialement été ajoutée localement sur AD53 :

<LocationMatch "^/pleade/iiif/.*/full/pct:100/0/default\.jpg$">
    Require all denied
</LocationMatch>

Cette règle fonctionnait mais ne protégeait qu'un serveur/site et uniquement le chemin `/pleade/iiif/`.

La protection a donc été déplacée vers les HAProxy AJLSM.

10.3. Protection HAProxy globale

[modifier | modifier le wikicode]

ACL :

acl iiif_full_image path_reg -i ^/(pleade|archives-en-ligne)/iiif/.*/full/pct:100/0/default\.jpg$

Blocage :

http-request deny deny_status 403 if iiif_full_image

Contrairement au moissonnage de métadonnées, il ne s'agit pas ici d'une limitation de fréquence.

Cette forme précise de demande est interdite directement.

Le code retourné est donc :

HTTP 403 Forbidden

et non :

HTTP 429 Too Many Requests

10.4. Validation du 8 octobre 2026

[modifier | modifier le wikicode]

Après déploiement HAProxy et après suppression de la règle Apache locale, les deux formes d'URL ont été testées.

Premier chemin :

curl -I \
'https://archives.lamayenne.fr/pleade/iiif/ark:/37963/r55nlf7bir4k/f99/full/pct:100/0/default.jpg'

Résultat :

HTTP/2 403
cache-control: no-cache
content-type: text/html

Deuxième chemin :

curl -I \
'https://archives.lamayenne.fr/archives-en-ligne/iiif/ark:/37963/r55nlf7bir4k/f99/full/pct:100/0/default.jpg'

Résultat :

HTTP/2 403
cache-control: no-cache
content-type: text/html

Point important : lors de ces deux tests, la règle Apache locale avait déjà été supprimée.

Ces tests valident donc sans ambiguïté le blocage par HAProxy.


11. Configuration HAProxy IIIF complète

[modifier | modifier le wikicode]

La configuration IIIF peut être résumée ainsi.

Backend :

# ============================================================
# Stick-table anti-moissonnage IIIF
# ============================================================

backend iiif_abuse
    stick-table type ip size 200k expire 30m store gpc0,gpc0_rate(10m) peers pleade_peers

Dans `frontend front_web` :

# ============================================================
# Protection anti-moissonnage IIIF
#
# Détection des requêtes massives sur :
#   - manifest.json
#   - info.json
#
# Les deux chemins IIIF utilisés sont pris en compte :
#   /pleade/iiif/...
#   /archives-en-ligne/iiif/...
#
# sc0 est déjà utilisé par la protection des recherches Pleade.
# sc1 est réservé au suivi IIIF.
#
# Une IP est bloquée si elle dépasse 400 requêtes IIIF
# de métadonnées sur une fenêtre de 10 minutes.
# ============================================================

acl iiif_metadata path_reg -i ^/(pleade|archives-en-ligne)/iiif/.*(/manifest\.json|/info\.json)$

# Suivi de l'IP dans la stick-table iiif_abuse
http-request track-sc1 src table iiif_abuse if iiif_metadata

# Incrément du compteur des requêtes IIIF de métadonnées
http-request sc-inc-gpc0(1) if iiif_metadata

# Détection du moissonnage massif
acl iiif_metadata_abuse sc_gpc0_rate(1) gt 400

# Réponse 429 avec la page personnalisée
http-request return status 429 content-type "text/html; charset=utf-8" lf-file /etc/haproxy/errors/429.html if iiif_metadata iiif_metadata_abuse


# ============================================================
# Blocage des téléchargements IIIF pleine résolution
#
# Interdit les demandes explicites d'image complète à 100 % :
#   /pleade/iiif/.../full/pct:100/0/default.jpg
#   /archives-en-ligne/iiif/.../full/pct:100/0/default.jpg
# ============================================================

acl iiif_full_image path_reg -i ^/(pleade|archives-en-ligne)/iiif/.*/full/pct:100/0/default\.jpg$

http-request deny deny_status 403 if iiif_full_image

12. Déploiement Ansible

[modifier | modifier le wikicode]

12.1. Validation préalable

[modifier | modifier le wikicode]

Toute modification doit d'abord être testée avec :

cd /Users/jlarvers/__SVN__/ansible-ajlsm/

ansible-playbook ./plays/deploy.haproxy.yml 
  -l ajlsm_loadbalancer 
  --check 
  --diff

`--check` simule les modifications.

`--diff` affiche les différences.

La tâche Ansible déploie la configuration avec validation préalable par HAProxy.

Une configuration syntaxiquement incorrecte n'est donc pas installée.

12.2. Déploiement réel

[modifier | modifier le wikicode]

Après validation :

ansible-playbook ./plays/deploy.haproxy.yml \
  -l ajlsm_loadbalancer \
  --diff

Attention :

--diff

n'est pas un mode de simulation.

Sans :

--check

les modifications sont réellement appliquées.

12.3. Résultat attendu

[modifier | modifier le wikicode]

Le récapitulatif doit notamment contenir :

unreachable=0
failed=0

sur :

haproxy01.ajlsm.lan
haproxy02.ajlsm.lan

13. Articulation avec les règles Apache

[modifier | modifier le wikicode]

Pour les sites hébergés derrière les HAProxy AJLSM, les règles suivantes n'ont plus vocation à être dupliquées dans Apache lorsqu'elles sont déjà appliquées globalement par HAProxy :

  • blocage comportemental massif de `manifest.json` / `info.json` ;
  • blocage de `/full/pct:100/0/default.jpg`.

Il faut éviter de maintenir simultanément une règle globale HAProxy et une multitude de règles Apache spécifiques par adresse IP.

Ainsi, la règle temporaire spécifique :

RewriteCond %{REMOTE_ADDR} ^54\.36\.103\.227$
RewriteCond %{REQUEST_URI} ^/pleade/iiif/.*(?:/manifest\.json|/info\.json)$ [NC]
RewriteRule ^ - [R=429,L]

a été supprimée après mise en service de la protection comportementale HAProxy.

De même, la règle :

<LocationMatch "^/pleade/iiif/.*/full/pct:100/0/default\.jpg$">
    Require all denied
</LocationMatch>

a été supprimée après validation du blocage global HAProxy.


14. Protection Apache / ModSecurity pour les installations extérieures

[modifier | modifier le wikicode]

Les chapitres suivants concernent principalement les serveurs Pleade qui ne sont pas protégés par les HAProxy AJLSM.

14.1. Objectif

[modifier | modifier le wikicode]

Cette protection a été mise en place à la suite de campagnes de requêtes automatisées distribuées provoquant une saturation de Pleade et d'Apache.

Le comportement observé était caractéristique d'une campagne coordonnée :

  • utilisation d'un grand nombre d'adresses IP différentes ;
  • changement possible de User-Agent ;
  • répétition du même tronc de recherche ;
  • variation systématique des facettes ;
  • requêtes individuellement peu nombreuses par adresse IP ;
  • génération de traitements Pleade coûteux pouvant provoquer des délais d'attente de 30 secondes côté Drupal puis des erreurs HTTP 500.

Un exemple observé était la répétition de :

query1=Mont Athos

depuis un grand nombre d'adresses IP, avec différentes combinaisons de facettes.

La protection ne cherche donc pas à identifier le robot uniquement par son adresse IP ou son User-Agent.

Elle analyse le comportement de recherche.

14.2. Pages concernées

[modifier | modifier le wikicode]

La protection s'applique aux pages publiques Pleade situées sous :

/archives-en-ligne/

dont le nom se termine par :

results.html

Exemples :

/archives-en-ligne/results.html
/archives-en-ligne/federate-results.html
/archives-en-ligne/cdc-results.html
/archives-en-ligne/tab-results.html
/archives-en-ligne/mosaic-results.html

La règle est volontairement générique afin de couvrir d'autres modes de résultats utilisant le même schéma de nommage.


15. ModSecurity

[modifier | modifier le wikicode]

Le serveur utilise ModSecurity 2.9.x.

Le moteur général reste en :

SecRuleEngine DetectionOnly

Les règles spécifiques à Pleade utilisent donc ModSecurity pour détecter le comportement et positionner une variable d'environnement.

Le blocage HTTP proprement dit est ensuite effectué par Apache/mod_rewrite.

Cela évite d'activer globalement les actions disruptives de ModSecurity et des règles OWASP CRS.

15.1. Fichier de configuration

[modifier | modifier le wikicode]
/etc/modsecurity/pleade-cluster-detection.conf

15.2. Détection d'un même tronc de recherche

[modifier | modifier le wikicode]

Le principe consiste à utiliser la valeur de `query1` comme clé d'une collection persistante `GLOBAL`.

Ainsi :

IP A -> query1=Mont Athos
IP B -> query1=Mont Athos
IP C -> query1=Mont Athos
IP D -> query1=Mont Athos

alimentent tous le même compteur.

Cela permet de détecter une campagne distribuée même lorsque chaque adresse IP ne fait qu'une seule requête.

15.3. Prise en compte des facettes

[modifier | modifier le wikicode]

Une recherche n'alimente le compteur que si elle comporte au moins deux paramètres de type :

fchampN=

Par exemple :

fchamp1=fpersname
fchamp2=fgeogname

Cette condition réduit le risque qu'une recherche simple et légitime très populaire soit considérée comme une campagne automatisée.


16. Gestion temporelle ModSecurity

[modifier | modifier le wikicode]

16.1. Première version : expirevar

[modifier | modifier le wikicode]

La première version utilisait :

expirevar:global.pleade_hits=120

Cette solution fonctionne mais ne constitue pas réellement une fenêtre glissante de 120 secondes.

À chaque nouvelle requête, l'expiration est repoussée.

Une campagne continue peut donc produire :

hits=100
hits=200
hits=300
hits=423
...

et le compteur ne disparaît que 120 secondes après l'arrêt des requêtes.

16.2. Variante avec deprecatevar

[modifier | modifier le wikicode]

Une évolution possible consiste à utiliser :

setvar:global.pleade_hits=+1,
deprecatevar:global.pleade_hits=10/120

`deprecatevar` introduit une décroissance temporelle.

Il ne s'agit toutefois pas d'une fenêtre glissante stricte donnant exactement le nombre de requêtes reçues pendant les 120 dernières secondes.

La valeur de décroissance doit être réglée après observation du trafic réel.

La version utilisant `expirevar` a été validée en production.

La variante `deprecatevar` doit être testée avant de remplacer définitivement la configuration existante.


17. Exemple de règle ModSecurity

[modifier | modifier le wikicode]
#
# Détection des campagnes distribuées Pleade
#

# 1 - Comptage
#
# Même query1 + au moins 2 facettes.
# Toutes les IP alimentent le même compteur associé à query1.

SecRule REQUEST_URI 
"^/archives-en-ligne/(?:[A-Za-z0-9_-]+-)?results\.html(?:\?|$)" 
"id:1100001,phase:1,pass,nolog,chain"
    SecRule ARGS_GET:query1 "!^$" 
        "initcol:global=%{ARGS_GET.query1},chain"
    SecRule &ARGS_GET:/^fchamp[0-9]+$/ "@ge 2" 
        "setvar:global.pleade_hits=+1,
         deprecatevar:global.pleade_hits=10/120"

# 2 - Détection
#
# Le seuil atteint provoque la création de la variable
# d'environnement utilisée ensuite par mod_rewrite.

SecRule GLOBAL:pleade_hits "@ge 10" 
"id:1100002,phase:1,pass,log,auditlog,
msg:'PLEADE_CLUSTER distributed search detected',
logdata:'query1=%{ARGS_GET.query1} hits=%{GLOBAL.pleade_hits} ip=%{REMOTE_ADDR}',
setenv:PLEADE_CLUSTER_BLOCK=1"

18. Blocage Apache

[modifier | modifier le wikicode]

Le fichier Apache utilisé est :

/etc/apache2/conf-available/block-complex-searches.conf

Lorsque le seuil ModSecurity est atteint :

PLEADE_CLUSTER_BLOCK=1

Apache peut appliquer :

# Blocage générique d'une campagne distribuée détectée par ModSecurity
RewriteCond %{ENV:PLEADE_CLUSTER_BLOCK} =1
RewriteRule ^ - [R=429,L]

La requête est ainsi arrêtée avant Drupal/Pleade.


19. Blocage de hpp

[modifier | modifier le wikicode]

Les recherches demandant plus de 1000 résultats sont bloquées.

# Bloquer les recherches Pleade avec plus de 1000 résultats demandés
#
# S'applique à toutes les pages publiques se terminant par results.html :
# results.html, federate-results.html, cdc-results.html,
# tab-results.html, mosaic-results.html, etc.

RewriteCond %{REQUEST_URI} ^/archives-en-ligne/(?:[A-Za-z0-9_-]+-)?results\.html$ [NC]
RewriteCond %{QUERY_STRING} "(^|&)hpp=(?:100[1-9]|10[1-9][0-9]|1[1-9][0-9]{2}|[2-9][0-9]{3}|[1-9][0-9]{4,})(?:&|$)" [NC]
RewriteRule ^ - [R=429,L]

Comportement :

hpp=1000     -> autorisé
hpp=1001     -> HTTP 429
hpp=10000    -> HTTP 429
hpp=1000000  -> HTTP 429

20. Blocage des recherches comportant au moins 6 facettes

[modifier | modifier le wikicode]
# Bloquer les recherches avec au moins 6 facettes

RewriteCond %{REQUEST_URI} ^/archives-en-ligne/(?:[A-Za-z0-9_-]+-)?results\.html$ [NC]
RewriteCond %{QUERY_STRING} "(?:^|&)fchamp[0-9]+=[^&]*(?:&[^&]*)*(?:&fchamp[0-9]+=[^&]*(?:&[^&]*)*){5}" [NC]
RewriteRule ^ - [R=429,L]

La numérotation des `fchamp` n'a pas besoin d'être continue.


21. Page HTTP 429 Apache

[modifier | modifier le wikicode]

La page statique est stockée dans :

/var/www/apache-errors/requete-trop-complexe.html

Configuration :

Alias /__ajlsm-errors/ /var/www/apache-errors/

<Directory "/var/www/apache-errors">
    Require all granted
</Directory>

ErrorDocument 429 /__ajlsm-errors/requete-trop-complexe.html

<Location "/__ajlsm-errors/">
    Require all granted
</Location>
chmod 755 /var/www/apache-errors
chmod 644 /var/www/apache-errors/requete-trop-complexe.html

Vérification :

namei -l /var/www/apache-errors/requete-trop-complexe.html

Le répertoire doit être traversable par `www-data`.

Le bit `x` du répertoire est indispensable.


22. Validation Apache

[modifier | modifier le wikicode]

Après toute modification :

apache2ctl configtest

Résultat attendu :

Syntax OK

Puis seulement :

systemctl reload apache2

Ne jamais recharger Apache si `configtest` retourne une erreur.


23. Test du mécanisme distribué ModSecurity

[modifier | modifier le wikicode]
for i in $(seq 1 12); do
    echo -n "$i -> "
    curl -s -o /dev/null -w '%{http_code}\n' \
      -H 'Host: salamandre.college-de-france.fr' \
      "http://127.0.0.1/archives-en-ligne/federate-results.html?query1=TESTBLOCK&fchamp1=a&fquery1=x&fchamp2=b&fquery2=$i"
done

Avec un seuil à 10, le résultat attendu est :

1 -> 200
2 -> 200
3 -> 200
4 -> 200
5 -> 200
6 -> 200
7 -> 200
8 -> 200
9 -> 200
10 -> 429
11 -> 429
12 -> 429

24. Surveillance ModSecurity

[modifier | modifier le wikicode]

Campagnes détectées

[modifier | modifier le wikicode]
grep -a 'PLEADE_CLUSTER distributed search detected' \
  /var/log/apache2/modsec_audit.log | tail -50

Recherche particulière

[modifier | modifier le wikicode]
grep -a 'PLEADE_CLUSTER distributed search detected' \
  /var/log/apache2/modsec_audit.log \
  | grep 'Mont Athos' \
  | tail -50

25. Protection spécifique contre les robots Meta

[modifier | modifier le wikicode]

Cette protection est complémentaire des mécanismes comportementaux.

Elle ne doit pas être considérée comme suffisante à elle seule puisqu'un robot peut modifier son User-Agent.

Méthode mod_rewrite

[modifier | modifier le wikicode]

Juste après :

RewriteEngine On
RewriteOptions inherit

ajouter :

# Blocage des crawlers Meta / Facebook
RewriteCond %{HTTP_USER_AGENT} "(meta-webindexer|meta-externalagent|meta-externalfetcher|facebookexternalhit|facebot|facebookbot)" [NC]
RewriteRule ^ - [F,L]

Variante SetEnvIfNoCase

[modifier | modifier le wikicode]
SetEnvIfNoCase User-Agent "meta-webindexer" meta_bot
SetEnvIfNoCase User-Agent "meta-externalagent" meta_bot
SetEnvIfNoCase User-Agent "meta-externalfetcher" meta_bot
SetEnvIfNoCase User-Agent "facebookexternalhit" meta_bot
SetEnvIfNoCase User-Agent "Facebot" meta_bot

<Location "/">
    <RequireAll>
        Require all granted
        Require not env meta_bot
    </RequireAll>
</Location>
for ua in \
  meta-webindexer \
  meta-externalagent \
  meta-externalfetcher \
  facebookexternalhit \
  facebot \
  facebookbot
do
    printf '%-25s -> ' "$ua"
    curl -s -o /dev/null -w '%{http_code}\n' \
      -H 'Host: pandor2026-preprod.ube.fr' \
      -A "$ua" \
      http://127.0.0.1/
done

Résultat attendu :

403

pour tous les User-Agents testés.

Contrôle négatif :

curl -s -o /dev/null -w '%{http_code}\n' \
  -H 'Host: pandor2026-preprod.ube.fr' \
  -A 'Mozilla/5.0' \
  http://127.0.0.1/

26. Résumé des protections

[modifier | modifier le wikicode]

Pour les sites derrière les HAProxy AJLSM :

                         Requête Internet
                                |
                                v
                         HAProxy-01 / 02
                                |
        +-----------------------+-----------------------+
        |                       |                       |
        v                       v                       v
 recherches Pleade       métadonnées IIIF       image IIIF complète
        |                       |                       |
        |                manifest.json                 |
        |                  info.json                   |
        |                       |                       |
        |                compteur par IP               |
        |                  sur 10 min                  |
        |                       |                       |
        |                 > 400 / 10 min       full/pct:100/0/
        |                       |               default.jpg
        |                       |                       |
        v                       v                       v
      HTTP 429                HTTP 429                HTTP 403
        |                       |                       |
        +-----------------------+-----------------------+
                                |
                         requête arrêtée
                                |
                   Apache / Drupal / Pleade
                         non sollicités

Pour les installations extérieures sans HAProxy AJLSM :

Requête
   |
   +-- hpp > 1000 --------------------------> HTTP 429
   |
   +-- >= 6 facettes -----------------------> HTTP 429
   |
   +-- même query1 + >= 2 facettes
            |
            v
       compteur GLOBAL ModSecurity
            |
            +-- activité faible
            |       |
            |       +--> expiration / décroissance
            |
            +-- activité importante
                    |
                    v
             seuil atteint
                    |
                    v
          PLEADE_CLUSTER_BLOCK=1
                    |
                    v
                 HTTP 429

27. Principes à retenir

[modifier | modifier le wikicode]

Les protections ne doivent pas reposer uniquement sur :

  • l'adresse IP ;
  • le réseau d'origine ;
  • le User-Agent ;
  • le navigateur déclaré.

Un robot peut :

  • changer d'adresse IP ;
  • répartir ses requêtes entre de nombreuses adresses ;
  • utiliser un User-Agent Chrome ou Firefox crédible ;
  • imiter partiellement le comportement d'un navigateur.

Il faut donc privilégier une analyse du comportement.

Dans l'infrastructure AJLSM :

  • les recherches Pleade anormales sont arrêtées au niveau HAProxy ;
  • le moissonnage massif de métadonnées IIIF est détecté par stick-table ;
  • le seuil IIIF initial est de 400 requêtes de métadonnées par IP sur 10 minutes ;
  • les deux chemins `/pleade/iiif/` et `/archives-en-ligne/iiif/` sont pris en compte ;
  • les téléchargements explicites `full/pct:100/0/default.jpg` sont interdits globalement ;
  • les compteurs sont partagés entre HAProxy-01 et HAProxy-02 ;
  • les règles temporaires spécifiques à une adresse IP doivent être supprimées une fois la protection comportementale validée.

L'objectif est toujours le même :

arrêter la requête anormale le plus tôt possible
                 |
                 v
éviter Apache / Drupal / Pleade / Tomcat
                 |
                 v
préserver CPU, mémoire, stockage et bande passante