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
Protection des recherches Pleade par HAProxy
[modifier | modifier le wikicode]Objet
[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.
Architecture
[modifier | modifier le wikicode]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]Principe
[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
Seuil
[modifier | modifier le wikicode]À 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.
Page HTTP 429
[modifier | modifier le wikicode]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.
Fichiers
[modifier | modifier le wikicode]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 '
HAProxy actif
[modifier | modifier le wikicode]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]hpp = 1000
[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'
hpp = 1001
[modifier | modifier le wikicode]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
Robot Meta
[modifier | modifier le wikicode]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.