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

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

De AJLSM
Aller à la navigation Aller à la recherche

Protection des recherches Pleade par HAProxy

[modifier | modifier le wikicode]

Cette configuration protège les interfaces publiques Pleade contre certaines requêtes susceptibles de provoquer une charge importante sur les serveurs applicatifs.

La protection est réalisée directement au niveau des deux HAProxy AJLSM, avant transmission des requêtes à Coraza puis aux serveurs applicatifs.

Elle couvre notamment :

  • les requêtes demandant un nombre excessif de résultats ;
  • les recherches utilisant un nombre excessif de facettes ;
  • les répétitions distribuées d'une même recherche ;
  • certains robots Meta / Facebook ;
  • l'affichage d'une page HTTP 429 explicite pour les utilisateurs légitimes.

La configuration est gérée par Ansible et versionnée dans SVN.


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

L'infrastructure HAProxy est redondante :

Serveur Adresse Rôle Port peers
haproxy-01 10.0.0.30 HAProxy 10000/TCP
haproxy-02 10.0.0.31 HAProxy 10000/TCP
VIP Keepalived 10.0.0.32 Adresse publique virtuelle -

Les connexions HTTP et HTTPS arrivent sur la VIP :

10.0.0.32:80
10.0.0.32:443

HAProxy transmet ensuite les requêtes vers Coraza via SPOE puis vers les backends correspondants.

La protection spécifique Pleade est volontairement exécutée avant Coraza afin de bloquer le plus tôt possible les requêtes manifestement excessives.

Synchronisation entre les deux HAProxy

[modifier | modifier le wikicode]

Certaines attaques sont distribuées entre plusieurs adresses IP.

Le suivi ne doit donc pas être effectué par adresse IP, mais à partir de la recherche elle-même.

De plus, comme le service repose sur deux HAProxy redondants, leurs compteurs doivent être synchronisés.

Déclaration des peers

[modifier | modifier le wikicode]

Dans la configuration HAProxy :

global
    stats socket /var/run/haproxy/admin.sock level admin mode 660
    localpeer {{ ansible_facts['hostname'] }}

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

Chaque HAProxy écoute donc sur TCP/10000 :

ss -lntp | grep ':10000 '

Exemple :

LISTEN 0 6 10.0.0.30:10000 0.0.0.0:* users:(("haproxy",...))

Le port 10000 est exclusivement destiné à la réplication des stick-tables HAProxy.

Vérification des peers

[modifier | modifier le wikicode]

La socket d'administration permet de contrôler la réplication :

echo "show peers" | socat stdio /var/run/haproxy/admin.sock

Une connexion correctement établie présente notamment :

last_status=ESTA
state=EST

Les compteurs de heartbeat doivent également évoluer :

tx_hbt=...
rx_hbt=...

Stick-table Pleade

[modifier | modifier le wikicode]

La table suivante est déclarée dans le frontend :

stick-table type string len 256 size 100k expire 120s store http_req_rate(120s) peers pleade_peers

Caractéristiques :

Paramètre Valeur Fonction
type string La clé est une chaîne de caractères
len 256 Longueur maximale de la clé
size 100k Jusqu'à environ 100 000 entrées
expire 120s Expiration des entrées
http_req_rate 120s Comptage des requêtes sur 120 secondes
peers pleade_peers Réplication entre les deux HAProxy

La table peut être inspectée avec :

echo "show table front_web" | socat stdio /var/run/haproxy/admin.sock

Exemple à vide :

# table: front_web, type: string, size:102400, used:0

Identification des pages de résultats Pleade

[modifier | modifier le wikicode]

Les protections ne doivent pas s'appliquer à l'ensemble du site.

Elles sont limitées aux pages de résultats Pleade :

acl pleade_results path_reg -i ^/(?:[a-z]{2}/)?archives-en-ligne/(?:[^/]+/)*(?:[A-Za-z0-9_-]+-)?results\.html$

Cette expression couvre notamment :

/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
/archives-en-ligne/mets/results.html
/fr/archives-en-ligne/results.html
/en/archives-en-ligne/results.html

Les appels internes de Pleade, par exemple :

/pleade/functions/sdx-facet-terms.xml

ne correspondent pas à cette ACL.

Une valeur importante de hpp utilisée par un appel interne Pleade n'est donc pas bloquée par ces règles.

Blocage des robots Meta / Facebook

[modifier | modifier le wikicode]

Les User-Agent suivants sont identifiés :

acl meta_bot hdr_sub(User-Agent) -i meta-webindexer
acl meta_bot hdr_sub(User-Agent) -i meta-externalagent
acl meta_bot hdr_sub(User-Agent) -i meta-externalfetcher
acl meta_bot hdr_sub(User-Agent) -i facebookexternalhit
acl meta_bot hdr_sub(User-Agent) -i facebot
acl meta_bot hdr_sub(User-Agent) -i facebookbot

Le blocage est limité aux pages de résultats Pleade :

http-request deny if meta_bot pleade_results

La réponse est HTTP 403.

Limitation du nombre de résultats

[modifier | modifier le wikicode]

Une recherche demandant plus de 1000 résultats est bloquée.

ACL :

acl pleade_hpp_too_high url_param(hpp) -m reg '^(?:100[1-9]|10[1-9][0-9]|1[1-9][0-9]{2}|[2-9][0-9]{3}|[1-9][0-9]{4,})$'

Exemples :

hpp Résultat
100 autorisé
1000 autorisé
1001 HTTP 429
2000 HTTP 429

La réponse est :

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

Limitation du nombre de facettes

[modifier | modifier le wikicode]

Une recherche contenant au moins six paramètres fchampN est bloquée.

ACL :

acl pleade_six_facets query -m reg -i '(^|&)fchamp[0-9]+=[^&]*(?:&[^&]*)*(?:&fchamp[0-9]+=[^&]*(?:&[^&]*)*){5}'

Réponse :

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

Ainsi :

  • 5 facettes : requête autorisée ;
  • 6 facettes ou plus : HTTP 429.

Détection d'une même recherche distribuée

[modifier | modifier le wikicode]

Une campagne automatisée peut répartir ses requêtes entre plusieurs adresses IP.

Une limitation fondée uniquement sur l'adresse IP serait donc inefficace.

La clé utilisée est construite à partir :

  • du domaine demandé ;
  • de la valeur de query1.

Exemple :

archives.lamayenne.fr|laval

Cela permet :

  • de ne pas mélanger deux clients utilisant la même recherche ;
  • de corréler des requêtes provenant d'adresses IP différentes ;
  • de synchroniser cette information entre les deux HAProxy.

Conditions de suivi

[modifier | modifier le wikicode]

La recherche doit posséder un query1 non vide :

acl pleade_query1_present url_param(query1),length gt 0

et au moins deux facettes :

acl pleade_two_facets query -m reg -i '(^|&)fchamp[0-9]+=[^&]*(?:&[^&]*)*&fchamp[0-9]+='

Construction de la clé

[modifier | modifier le wikicode]
http-request set-var-fmt(txn.pleade_key) \
    "%[req.hdr(host),host_only]|%[url_param(query1),url_dec(1),lower]" \
    if pleade_results pleade_query1_present pleade_two_facets

La valeur de query1 est :

  • décodée ;
  • passée en minuscules.

Alimentation de la stick-table

[modifier | modifier le wikicode]
http-request track-sc0 var(txn.pleade_key) \
    if pleade_results pleade_query1_present pleade_two_facets

À partir de 10 occurrences dans la fenêtre de suivi :

http-request return status 429 \
    content-type "text/html; charset=utf-8" \
    lf-file /etc/haproxy/errors/429.html \
    if pleade_results pleade_query1_present pleade_two_facets \
       { sc_http_req_rate(0) ge 10 }

Cette protection fonctionne indépendamment de l'adresse IP du client.

Ordre de traitement

[modifier | modifier le wikicode]

Les protections Pleade sont placées avant Coraza :

# Protections Pleade
[...]

# Coraza
filter spoe engine coraza config /etc/haproxy/coraza.cfg
http-request send-spoe-group coraza coraza-req

Cela permet de rejeter les requêtes coûteuses sans les transmettre inutilement au WAF ni au backend applicatif.

La page commune est :

/etc/haproxy/errors/429.html

Elle est utilisée par les trois protections :

  • hpp supérieur à 1000 ;
  • six facettes ou plus ;
  • répétition distribuée d'une même recherche.

HAProxy interprète le fichier comme un log-format file via lf-file.

Il est donc possible d'y utiliser des variables HAProxy.

Exemple :

<title>Requête bloquée | %[req.hdr(host),host_only]</title>

ou :

<strong>%[req.hdr(host),host_only]</strong>

Une même page peut ainsi être utilisée pour tous les clients.

Contacts par site

[modifier | modifier le wikicode]

Les adresses de contact sont définies dans :

/etc/haproxy/maps/pleade-contact.map

Contenu actuel :

archives.lamayenne.fr archives@lamayenne.fr
inventaires-archives.loire-atlantique.fr archives@loire-atlantique.fr
earchives.le64.fr archives@le64.fr
ens-lyon.pleade.com archives@ens-lyon.fr
archives.agglo-pau.fr archives@agglo-pau.fr

HAProxy charge le contact correspondant au Host :

http-request set-var(txn.pleade_contact) \
    req.hdr(host),host_only,map_str(/etc/haproxy/maps/pleade-contact.map,arvers@ajlsm.com)

Une adresse de repli est donc utilisée lorsqu'un domaine n'existe pas dans la map.

Courriel de signalement

[modifier | modifier le wikicode]

Le bouton :

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

est complété par Javascript lorsque la page est chargée.

Le script récupère :

  • le domaine du site ;
  • l'adresse de contact déterminée par HAProxy ;
  • l'URL complète ayant provoqué le blocage.

Extrait :

document.addEventListener('DOMContentLoaded', function () {

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

  const urlBloquee = window.location.href;
  const nomSite = window.location.hostname;
  const adresseContact = '%[var(txn.pleade_contact)]';

  zoneUrl.textContent = urlBloquee;

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

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

  lien.href =
    'mailto:' + adresseContact +
    '?subject=' + encodeURIComponent(sujet) +
    '&body=' + encodeURIComponent(corps);
});

Le courriel obtenu contient donc automatiquement l'URL ayant provoqué le blocage.

Gestion par Ansible

[modifier | modifier le wikicode]

La configuration ne doit pas être modifiée directement sur les HAProxy.

La source de vérité est le dépôt Ansible/SVN.

Configuration HAProxy :

inventory/group_vars/ajlsm_loadbalancer/haproxy.yml

Rôle :

roles/haproxy/

Les fichiers spécifiques à la protection Pleade sont :

roles/haproxy/files/429.html
roles/haproxy/files/pleade-contact.map

Les tâches sont définies dans :

roles/haproxy/tasks/main.yml

Déploiement des fichiers auxiliaires

[modifier | modifier le wikicode]

Les répertoires sont créés automatiquement :

- name: "Create HAProxy auxiliary directories"
  file:
    path: "{{ item }}"
    state: "directory"
    owner: root
    group: root
    mode: "0755"
  loop:
    - /etc/haproxy/errors
    - /etc/haproxy/maps

La page 429 :

- name: "Deploy Pleade 429 error page"
  copy:
    src: "429.html"
    dest: "/etc/haproxy/errors/429.html"
    owner: root
    group: root
    mode: "0644"
  notify: "restart haproxy"

La map :

- name: "Deploy Pleade contact map"
  copy:
    src: "pleade-contact.map"
    dest: "/etc/haproxy/maps/pleade-contact.map"
    owner: root
    group: root
    mode: "0644"
  notify: "restart haproxy"

Ces tâches doivent être exécutées avant le déploiement de haproxy.cfg, car la validation de celui-ci référence les deux fichiers.

Validation de la configuration

[modifier | modifier le wikicode]

La configuration principale est déployée par :

- name: "Deploy configuration and validate it"
  register: "haproxy_deploy"
  template:
    src: "haproxy.cfg.j2"
    dest: "/etc/haproxy/haproxy.cfg"
    mode: 0644
    validate: "haproxy -f %s -c -V -q"
    backup: true
  notify: "restart haproxy"

La commande validate est importante : Ansible demande à HAProxy de valider le fichier généré avant de remplacer la configuration existante.

Une erreur de syntaxe empêche donc le déploiement de la nouvelle configuration.

Prévisualisation avant déploiement

[modifier | modifier le wikicode]

Depuis le dépôt Ansible :

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

Cela permet notamment de contrôler :

  • les modifications de haproxy.cfg ;
  • les modifications de 429.html ;
  • les modifications de pleade-contact.map.

Attention : le mode --check ne remplace pas systématiquement une validation réelle par HAProxy.

Pour une modification importante, déployer d'abord sur le HAProxy passif.

Déploiement prudent

[modifier | modifier le wikicode]

HAProxy passif

[modifier | modifier le wikicode]

Exemple :

ansible-playbook ./plays/deploy.haproxy.yml \
  -l haproxy02.ajlsm.lan

Puis :

haproxy -c -V -f /etc/haproxy/haproxy.cfg
systemctl status haproxy --no-pager
ss -lntp | grep ':10000 '

Après validation du premier nœud :

ansible-playbook ./plays/deploy.haproxy.yml \
  -l haproxy01.ajlsm.lan

Puis effectuer les mêmes contrôles.

Avertissements HAProxy connus

[modifier | modifier le wikicode]

La validation peut actuellement produire des avertissements du type :

a 'http-request' rule placed after a 'use_backend' rule will still be processed before.

Ils ne rendent pas la configuration invalide.

HAProxy indique que les règles http-request concernées seront malgré leur position textuelle exécutées avant les règles use_backend.

Ces avertissements correspondent à l'organisation actuelle de la configuration et pourront être nettoyés séparément.

Tests fonctionnels

[modifier | modifier le wikicode]

Doit rester autorisé :

curl -sk -o /dev/null -w '%{http_code}\n' \
  --resolve 'archives.lamayenne.fr:443:10.0.0.32' \
  'https://archives.lamayenne.fr/archives-en-ligne/results.html?query1=test&hpp=1000'

Doit retourner 429 :

curl -skI \
  --resolve 'archives.lamayenne.fr:443:10.0.0.32' \
  'https://archives.lamayenne.fr/archives-en-ligne/results.html?query1=test&hpp=1001'

Résultat attendu :

HTTP/2 429
content-type: text/html; charset=utf-8
curl -sk -o /dev/null -w '%{http_code}\n' \
  --resolve 'archives.lamayenne.fr:443:10.0.0.32' \
  -A 'meta-webindexer' \
  'https://archives.lamayenne.fr/archives-en-ligne/results.html?query1=test'

Résultat attendu :

403

Vérification de la page personnalisée

[modifier | modifier le wikicode]
curl -sk \
  --resolve 'archives.lamayenne.fr:443:10.0.0.32' \
  'https://archives.lamayenne.fr/archives-en-ligne/results.html?query1=test&hpp=1001'

La réponse doit notamment contenir :

<title>Requête bloquée | archives.lamayenne.fr</title>

L'adresse utilisée pour le signalement doit être :

archives@lamayenne.fr

Contrôle de la réplication

[modifier | modifier le wikicode]

Sur haproxy-01 :

echo "show peers" | socat stdio /var/run/haproxy/admin.sock
echo "show table front_web" | socat stdio /var/run/haproxy/admin.sock

Puis effectuer les mêmes commandes sur haproxy-02.

Après une recherche suivie, la clé correspondante doit être répliquée dans la stick-table de l'autre HAProxy.

Exemple de clé :

archives.lamayenne.fr|laval

Validation de la réplication

[modifier | modifier le wikicode]

La réplication a été validée en production le 24 septembre 2026.

Deux séries de requêtes simultanées ont été exécutées sur la même recherche de test :

archives.lamayenne.fr|test-haproxy-0924

Les 24 requêtes cumulées ont alimenté un compteur unique.

Les deux HAProxy ont ensuite présenté exactement la même entrée :

key=archives.lamayenne.fr|test-haproxy-0924
http_req_rate(120000)=24

Le seuil HTTP 429 a été déclenché dès que le nombre cumulé de requêtes a atteint le seuil configuré.

Ce test valide :

  • le suivi par domaine + query1 ;
  • le comptage sur 120 secondes ;
  • le déclenchement HTTP 429 ;
  • la réplication de la stick-table entre haproxy-01 et haproxy-02.

Ajout d'un nouveau client

[modifier | modifier le wikicode]

Pour un nouveau site Pleade hébergé derrière ces HAProxy, ajouter son domaine et son adresse de contact dans :

roles/haproxy/files/pleade-contact.map

Format :

nom.domaine.fr adresse-contact@example.fr

Puis contrôler :

svn diff

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

et déployer après validation.

Versionnement SVN

[modifier | modifier le wikicode]

Contrôler les modifications :

svn status
svn diff

Les fichiers concernés par cette fonctionnalité sont actuellement :

inventory/group_vars/ajlsm_loadbalancer/haproxy.yml
roles/haproxy/tasks/main.yml
roles/haproxy/files/429.html
roles/haproxy/files/pleade-contact.map

Après validation :

svn commit -m "HAProxy: protection Pleade 429, peers, page erreur et contacts"

Résumé des réponses HTTP

[modifier | modifier le wikicode]
Situation Réponse
Recherche normale Transmise normalement
hpp <= 1000 Autorisée
hpp > 1000 429
Moins de 6 facettes Autorisée par cette règle
6 facettes ou plus 429
Même query1 avec au moins 2 facettes, seuil atteint 429
Robot Meta/Facebook sur résultats Pleade 403
Appel interne /pleade/functions/... Non concerné par ces règles

Points de maintenance

[modifier | modifier le wikicode]
  • Ne pas modifier directement 429.html ou pleade-contact.map sur les serveurs : modifier les fichiers du rôle Ansible.
  • Déployer de préférence d'abord sur le nœud passif lors d'une modification importante.
  • Toujours contrôler la configuration avec haproxy -c.
  • Vérifier show peers après une modification de la configuration des peers.
  • Vérifier show table front_web pour diagnostiquer le suivi des recherches.
  • Ajouter tout nouveau domaine Pleade à pleade-contact.map.
  • Les avertissements relatifs aux http-request placés après des use_backend restent à nettoyer ultérieurement.