Anubis, l'outil qui bloque la récolte agressive

Le web est un vaste terrain de jeu. Que ce soit des humains ou des robots, il y a une quantité de données et de flux qui se croisent et se bousculent.

Un peu de contexte#

Être visible et accessible sur Internet est formidable, mais cela signifie aussi qu’il est important de prendre des mesures de sécurité pour éviter les soucis.

Que vous fassiez appel à un professionnel pour héberger vos applications et services ou que vous rendiez votre serveur en ligne public, vous allez rencontrer des attaques automatisées et parfois ciblées. Ces attaques sont fréquentes, mais heureusement, il existe des outils pour les limiter et de temps en temps les contrer.

Des entreprises comme Cloudflare , Sucuri ou Akamai proposent des solutions pour se protéger contre des attaques courantes, comme le déni de service ou les pare-feu applicatifs (WAF), si vous êtes déjà en ligne. En France, des prestataires comme OVHcloud ou Scaleway offrent des services similaires, avec des options payantes selon l’offre de service.

Cependant, que vous soyez en ligne ou en local et que vous gériez vous-même l’infrastructure, vous devrez gérer la sécurité et les attaques qui en découlent. L’arrivée de l’IA ne simplifie pas les choses, car les robots peuvent apprendre et s’améliorer en permanence, contrairement aux humains. Même si vous avez configuré votre serveur web avec toutes les protections habituelles, comme les headers, les méthodes autorisées et les listes de blocage, cela ne suffit plus aujourd’hui.

J’omets volontairement tous les robots de récolte de données, ce n’est pas nouveau et même présent depuis le début d’internet. Las de cette situation suite à une interruption de son Gitea , Xe a décidé de créer un outil de blocage des robots , principalement ceux dédiés à l’entrainement des modèles pour les IA.

Anubis n’est pas un dieu#

Anubis, un outil développé en Go, présente un défi que le navigateur doit surmonter pour accéder et afficher la page sous-jacente. En cas d’échec, l’accès à la page est refusé. Pour plus de détails, consultez le site officiel de l’application à l’adresse https://anubis.techaro.lol/ .

Anubis: Web AI Firewall Utility | AnubisWeigh the soul of incoming HTTP requests to protect your website!> Anubis is a man-in-the-middle HTTP proxy that requires clients to either solve or have solved a proof-of-work challenge before they can access the site. This is a very simple way to block the most common AI scrapers because they are not able to execute JavaScript to solve the challenge.

Contexte d’utilisation#

Pour ma part, j’expose ce site web depuis un serveur en ligne ; je fais confiance à l’hébergeur pour me protéger des attaques type DDoS. Cependant, il n’est pas en mesure et ce n’est pas dans ces attributions que de me protéger des attaques plus travaillées (scraping et autres joyeusetés). Je vais alors mettre en place Anubis dans ma pile docker compose pour l’intégrer entre Traefik et ce site.

Toutes les applications que j’exposerai sur la toile seront protégées par Anubis.

Mise en place d’Anubis#

Il n’existe pas de middleware Anubis officiel pour Traefik, par conséquent plusieurs configurations s’imposent. Je vais continuer d’utiliser une configuration “dynamique” (rappel à cette adresse , rubrique “Qu’est-ce que la configuration dynamique ?”) pour effectuer la mise en place. J’utiliserai le port d’origine d’Anubis (TCP/3923). L’arborescence des fichiers est la suivante :

/opt/docker
.
├── conf
│   ├── acme.json
│   ├── anubis_private_key
│   ├── conftraefik.yml
│   ├── ghost.config.production.json
│   ├── traefikdynamic
│   │   ├── anubis.yml
│   │   ├── ghost.yml
├── docker-compose.yml
├── logs
│   ├── ghost
│   ├── traefik.log

Le fichier docker-compose.yml à la racine de /opt/docker comporte la déclaration des services (les applications) que nous allons déployer. Le dossier conf contient les configurations des applications, tandis que le dossier logs est destiné aux journaux.

Déploiement de Traefik, Anubis et Ghost#

Voici le fichier docker-compose.yml qui défini la pile technique dont nous avons besoin :

---
services:
  traefik:
    container_name: traefik
    image: traefik:chaource
    networks:
      - front
      - backenddc
    restart: always
    ports:
      - "443:443/tcp"
      - "443:443/udp"
      - "80:80/tcp"
    environment:
      - TZ=Europe/Paris
    volumes:
      - ./conf/conftraefik.yml:/etc/traefik/traefik.yml:ro
      - ./conf/traefikdynamic:/dynamic
      - ./logs/traefik.log:/etc/traefik/traefik.log
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./conf/acme.json:/acme.json

  anubis:
    container_name: anubis
    image: ghcr.io/techarohq/anubis:main
    restart: unless-stopped
    environment:
      BIND: ":8080"
      TARGET: "http://traefik:3923"
      DIFFICULTY: "4"
      ED25519_PRIVATE_KEY_HEX_FILE: "/etc/anubis_private_key"
    networks:
      - backenddc
    ports:
      - 8080:8080
    volumes:
      - ./conf/anubis_private_key:/etc/anubis_private_key:ro
    healthcheck:
      test: ["CMD", "anubis", "--healthcheck"]
      interval: 5s
      timeout: 30s
      retries: 5
      start_period: 500ms

  ghostdb:
    cap_add: [SYS_NICE]
    image: mysql:8.0-debian
    container_name: ghostdb
    restart: unless-stopped
    networks:
      - backendghost
    volumes:
      - ghost_sqldata:/var/lib/mysql
    command: --innodb_use_native_aio=0
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_DATABASE: ghostappdb
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 20s
      retries: 10

  ghostapp:
    image: ghost:5.130
    container_name: ghostapp
    restart: unless-stopped
    cap_add:
      - SYS_PTRACE
    networks:
      - backendghost
      - front
    volumes:
      - ./conf/ghost.config.production.json:/var/lib/ghost/config.production.json:ro
      - ./logs/ghost:/var/lib/ghost/logs
      - ghost_app:/var/lib/ghost/content
    environment:
      TZ: Europe/Paris
    depends_on:
      ghostdb:
        condition: service_healthy

volumes:
  ghost_app:
  ghost_sqldata:

networks:
  front:
    driver: bridge
  backenddc:
  backendghost:

Configuration de Traefik#

Passons aux fichiers de configuration de Traefik :

Fichier conftraefik.yml (le fichier de configuration principal) :

global:
  sendAnonymousUsage: true
  checkNewVersion: false

api:
  dashboard: true
#  debug: true

log:
  level: INFO
  filePath: "/etc/traefik/traefik.log"
  format: common
  maxSize: 12
  maxAge: 60
  compress: true

providers:
  docker:
    endpoint: unix:///var/run/docker.sock
    exposedByDefault: false
    watch: true
  file:
    directory: "/dynamic"
    watch: true

entryPoints:
  web:
    address: ":80"

  websecure:
    address: ":443"
   http2:
     maxConcurrentStreams: 250
    http3:
      advertisedPort: 443
   transport:
     keepAliveMaxRequests: 120
     keepAliveMaxTime: 20s

  # Anubis
  anubis:
    address: ":3923"

certificatesResolvers:
  letsencrypt:
    acme:
      email: contact@domain.local
      caServer: https://acme-staging-v02.api.letsencrypt.org/directory
#      caServer: https://acme-v02.api.letsencrypt.org/directory
      storage: "/etc/traefik/acme.json"
      keyType: EC256
      httpChallenge:
        entryPoint: web

Le mode de configuration choisi pour Traefik est “dynamique”, la configuration est chargée à chaud dès qu’une modification est faite. Dans le dossier /opt/docker/conf/traefikdynamic, je vais créer deux fichiers, un pour Anubis et un autre pour Ghost.

Fichier /opt/docker/conf/traefikdynamic/anubis.yml :

http:
  services:
    svc-anubis:
      loadBalancer:
        servers:
          - url: "http://anubis:8080"

  routers:
    rt-anubis:
      entryPoints:
        - "websecure"
      rule: "(Host(`exemple.fr`) || Host(`blog.exemple.fr`))"
      service: "svc-anubis"
      middlewares:
        - mw-headers-anubis
      tls:
        certResolver: letsencrypt
        domains:
          - main: "exemple.fr"
            sans:
              - "*.exemple.fr"

    mw-headers-anubis:
      headers:
        hostsProxyHeaders:
          - "X-Forwarded-Host"
          - "X-Forwarded-Proto"
          - "X-Forwarded-For"
        customRequestHeaders:
          X-Forwarded-Proto: "https"

Fichier /opt/docker/conf/traefikdynamic/ghost.yml :

http:
  services:
    svc-ghost:
      loadBalancer:
        servers:
          - url: "http://ghostapp:2368"

  routers:
    rt-ghost:
      entryPoints:
        #- "websecure"
        - "anubis"
      rule: "Host(`blog.exemple.fr`)"
      service: "svc-ghost"
      middlewares:
        - mw-headers-ghost

    mw-headers-ghost:
      headers:
        hostsProxyHeaders:
          - "X-Forwarded-Host"
          - "X-Forwarded-Proto"
          - "X-Forwarded-For"
        customRequestHeaders:
          X-Forwarded-Proto: "https"
          X-Forwarded-Host: "blog.exemple.fr"

Il y a un routeur et un service par application ; Traefik écoute sur les ports TCP/80, TCP/443 et UDP/443 (HTTP3 QUIC) et réceptionnera les requêtes depuis l’extérieur. Le port d’Anubis (3923) ne doit pas être exposé sur le web, seulement en interne. Ensuite, selon le nom de domaine ciblé, Traefik redirigera vers le routeur Anubis, puisqu’il écoute sur le port “websecure” (TCP/443 et UDP/443). Le routeur rt-ghost écoute sur le port “anubis”, et c’est là que la magie opère : une redirection du flux sera faite si les conditions sont remplies (challenge réussi, nom de domaine (rule) correspondant entre les routeurs Anubis et Ghost).

J’ai ajouté des middleware, notamment pour les headers. En faisant les essais sur mon site, je me suis heurté à des problèmes de redirection, des changements d’URL et de protocole qui empêchaient le bon fonctionnement. Avec la définition des headers “Host”, “Proto” et “For”, il n’y a plus de place au doute (et ça marche).

Configuration de Ghost#

Une configuration simple :

{
  "name": "J.HOMMET.NET",
  "url": "http://ghostapp:2368",
  "database": {
    "client": "mysql",
    "connection": {
      "host": "ghostdb",
      "user": "user",
      "port": "3306",
      "password": "password",
      "database": "ghostappdb"
    }
  },
  "server": {
    "port": 2368,
    "host": "0.0.0.0"
  },
  "privacy": {
    "useUpdateCheck": false,
    "useGravatar": false,
    "useRpcPing": true,
    "useStructuredData": true
  },
  "logging": {
    "path": "/var/lib/ghost/logs/",
    "useLocalTime": true,
    "level": "info",
    "rotation": {
      "enabled": true,
      "count": 15,
      "period": "1d"
    },
    "transports": ["stdout", "file"]
  },
  "paths": {
    "contentPath": "/var/lib/ghost/content"
  }
}

Démarrez les services, regardez les logs de démarrage des conteneurs et testez l’accès à vos services via un navigateur web.

docker compose up -d
docker compose logs -f

Si tout est en ordre, lors de l’accès à votre service, vous devriez voir cette fenêtre :

“It works” comme dirait Apache 2 😀.

Dans cet article, j’utilise une seule instance Anubis pour différents services. Il agit comme un “catch ’em all”. Vous pourriez avoir une instance Anubis par service, permettant d’affiner et de spécialiser la configuration pour chaque service. Toutefois, ce mode de fonctionnement pourrait être consommateur de performance et demande une administration plus accrue, qui n’est pas forcément utile ni pratique.

Sources#

#traefik #anubis
Julien HOMMET
7 minutes
1366 mots
tuto