Ce guide décrit une intégration où Fail2ban continue d'effectuer son bannissement local habituel et envoie en parallèle une observation à IP Blocklist Manager.

1. Ce que fait l'API

Endpoint :

POST /api/v1/reports
Authorization: Bearer <clé reports:create>
Content-Type: application/json

Créer une source et une clé distinctes par serveur dans l'administration. Pour un serveur uniquement producteur de rapports, limiter la clé à reports:create.

Corps minimal :

{
  "network": "203.0.113.42",
  "category": "ssh_bruteforce",
  "reason": "Fail2ban : bannissement dans la jail sshd"
}

Corps conseillé :

{
  "network": "203.0.113.42",
  "category": "ssh_bruteforce",
  "reason": "Fail2ban : bannissement dans la jail sshd",
  "services": [
    {"protocol": "tcp", "port": 22}
  ],
  "metadata": {
    "producer": "fail2ban",
    "jail": "sshd",
    "attempts": 8,
    "bantime_seconds": 3600,
    "host": "web01"
  }
}

Contraintes à conserver dans tous les scripts :

  • un objet JSON par requête, pas de batch ;
  • maximum 64 Kio pour le rapport ;
  • network, category, reason sont obligatoires ;
  • category utilise [a-z0-9][a-z0-9_-]* ;
  • chaque service utilise uniquement protocol, port, éventuellement application_id ;
  • le port doit être un entier JSON ;
  • ne jamais envoyer source_id, reported_at, blocked, expires_at, slug ou un ID métier non prévu ;
  • le serveur normalise les IP en /32 ou /128 et les CIDR sur l'adresse réseau.

2. Point essentiel : rapport ≠ blocage

Le rapport est une observation historique ; avec REPORT_AUTO_BLOCK_ENABLED=true, le POST crée également un blocage local distinct, de 12 h par défaut (REPORT_AUTO_BLOCK_HOURS, minimum 12 h). Les rapports répétés prolongent le blocage automatique du même réseau/service. Les réseaux chevauchant Never Block restent seulement rapportés. Le POST :

  • crée une décision par service rapporté, ou globale sans service ;
  • ne lance pas de génération ;
  • ne publie pas une liste ;
  • fixe une expiration serveur ; les commandes périodiques d’expiration et de génération actualisent les exports ;
  • ne supprime rien lors d'un unban Fail2ban.

Il est donc recommandé d'ajouter le reporter comme deuxième action et de conserver le banaction actuel de Fail2ban.

3. Installation du reporter

Copier :

install -m 0755 reporter/blocklist-report.py /usr/local/sbin/blocklist-report.py
install -d -m 0700 /etc/blocklist-reporter
install -m 0600 reporter/config.json.example /etc/blocklist-reporter/config.json

Modifier /etc/blocklist-reporter/config.json :

{
  "api_base": "https://blocklist.example.net",
  "api_key_file": "/etc/blocklist-reporter/api.key",
  "timeout_seconds": 20
}

Puis déposer uniquement la clé dans :

printf '%s\n' 'blk_prod_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  > /etc/blocklist-reporter/api.key
chmod 600 /etc/blocklist-reporter/api.key

Ne pas mettre la clé dans jail.local, dans l'URL, dans les arguments de curl, dans Git ou dans les logs.

4. Test manuel

/usr/local/sbin/blocklist-report.py \
  --network 203.0.113.42 \
  --category ssh_bruteforce \
  --jail sshd \
  --services tcp:22 \
  --failures 8 \
  --bantime 3600

Le script :

  1. valide l'IP/CIDR et les services ;
  2. construit le JSON avec l'encodeur Python ;
  3. lit la clé depuis le fichier secret ;
  4. envoie un unique POST ;
  5. n'effectue aucun retry automatique après timeout ou HTTP 5xx.

Ce dernier choix est volontaire : l'API actuelle n'a pas de clé d'idempotence. Après un timeout ou certaines erreurs serveur, il peut être impossible de savoir si le rapport a déjà été enregistré. Un retry aveugle pourrait donc créer un doublon historique.

5. Action Fail2ban

Copier :

install -m 0644 fail2ban/action.d/blocklist-report.conf \
  /etc/fail2ban/action.d/blocklist-report.conf

Extrait :

[Definition]
actionban = /usr/local/sbin/blocklist-report.py --network <ip> --category "<category>" --jail "<name>" --services "<services>" --failures <failures> --bantime <bantime>
actionunban =

[Init]
category = abuse
services =

Fail2ban exécute les actionban lorsqu'une IP est bannie. L'action fournie est volontairement auxiliaire : elle ne comporte aucun actionunban, car un rapport historique n'est pas supprimé à la fin du ban.

6. Ajouter le reporter à une jail existante

Exemple SSH :

[sshd]
enabled = true
action = %(action_)s
         blocklist-report[category=ssh_bruteforce,services="tcp:22"]

%(action_)s conserve l'action de bannissement standard de la distribution. Si la jail utilise déjà une action personnalisée, conserver cette action et ajouter blocklist-report[...] sur une ligne supplémentaire.

7. Choisir les catégories

Les catégories ne sont pas limitées à une liste fermée. Quelques conventions cohérentes avec le contrat actuel :

Evénement Catégorie suggérée
brute force SSH ssh_bruteforce
scan de chemins HTTP / vulnérabilités web_scan
tentatives répétées d'identifiants HTTP/SIP/SMTP/IMAP credential_stuffing
spam / comportement SMTP abusif spam
scan réseau multi-ports port_scan
bot abusif générique abuse
cas non classé other

Ce sont des conventions d'intégration, pas une taxonomie fermée imposée par l'API.

8. Gestion des erreurs

Statut Interprétation côté reporter
201 rapport accepté
400 / 422 configuration ou payload à corriger
401 / 403 clé/source/permission à corriger
413 rapport trop volumineux
429 quota atteint ; respecter Retry-After si une reprise contrôlée est mise en place
500 / timeout résultat potentiellement incertain ; éviter le retry aveugle

Le quota de création documenté par défaut est de 120 requêtes par 60 secondes et par ID de clé. Pour des jails très bruyantes, ajuster la détection Fail2ban et/ou les quotas plutôt que d'envoyer plusieurs reports pour la même séquence d'événements.

9. Validation Fail2ban

Après modification :

fail2ban-client -t
systemctl reload fail2ban
fail2ban-client status
fail2ban-client status sshd

Déclencher ensuite un test depuis une IP de laboratoire autorisée et vérifier :

  • le ban local ;
  • les journaux Fail2ban ;
  • le journal du reporter ;
  • la présence du rapport dans IP Blocklist Manager.

Ne jamais effectuer le test depuis l'unique IP d'administration sans accès de secours.

10. Pourquoi le script encode le JSON lui-même

Ne pas écrire une action de ce type :

# À éviter
actionban = curl ... --data '{"network":"<ip>","reason":"<matches>"}'

Des valeurs interpolées dans du JSON par concaténation peuvent casser l'encodage ou introduire du contenu non échappé. Le helper Python valide les types et passe par json.dumps().

Fichiers prêts à adapter