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,reasonsont obligatoires ;categoryutilise[a-z0-9][a-z0-9_-]*;- chaque service utilise uniquement
protocol,port, éventuellementapplication_id; - le port doit être un entier JSON ;
- ne jamais envoyer
source_id,reported_at,blocked,expires_at,slugou un ID métier non prévu ; - le serveur normalise les IP en
/32ou/128et 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
unbanFail2ban.
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 :
- valide l'IP/CIDR et les services ;
- construit le JSON avec l'encodeur Python ;
- lit la clé depuis le fichier secret ;
- envoie un unique POST ;
- 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().