CrowdSec et Nginx Proxy Manager : protéger Docker sans image custom

Une licorne Geekeries bloque un paquet web suspect avec CrowdSec Nginx Proxy Manager devant des conteneurs Docker

Salut les Geeks ! Après avoir protégé les Basic Auth avec Fail2ban devant Nginx Proxy Manager, je voulais compléter la défense contre les scans et comportements HTTP plus larges. Le résultat est un filtrage CrowdSec pour mon Nginx Proxy Manager qui conserve l’image officielle du proxy, observe d’abord, puis bloque les décisions au firewall, y compris pour les flux Docker.

Comme vous allez le voir, ce n’est pas un WAF HTTP. CrowdSec apprend à partir de l’activité déjà vue par le proxy qui lui est transféré, puis le firewall refuse les adresses IP décidées comme malveillantes depuis les liste souscrites chez CrowdSec. C’est moins riche qu’une inspection HTTP intégrée, mais c’est une solution simple, maintenable et adaptée à une installation NPM Docker existante.

Pourquoi je n’ai pas fabriqué une image NPM avec un bouncer Nginx

La première piste semblait évidente : intégrer le bouncer CrowdSec directement dans Nginx Proxy Manager. En pratique, cela suppose l’installation de Lua ou d’OpenResty, des modules spécifiques : donc une image dérivée de NPM, un fork ou une image communautaire. Certaines recettes existantes reposent sur des images peu suivies. Pour un reverse proxy exposé sur Internet, je ne voulais ni dépendre d’une image custom, ni transformer chaque mise à jour NPM en opération de compatibilité.

J’ai donc conservé l’image officielle de Nginx Proxy Manager. CrowdSec tourne sur l’hôte et lit les logs NPM depuis le stockage persistant prévu par NPM. Cette acquisition côté hôte est le compromis : aucune modification de l’image, aucun socket Docker à fournir, aucun conteneur privilégié supplémentaire, mais un filtrage au niveau du direwall iptables et pas du serveur web nginx.

Le choix est volontairement KISS : le proxy reste un proxy, CrowdSec détecte, et le firewall applique les décisions IP.

Ce que fait réellement ce montage

Avec CrowdSec Nginx Proxy Manager, le moteur analyse les accès NPM avec la collection officielle pour Nginx Proxy Manager. Ses scénarios peuvent relever des scans, des tentatives d’exploitation et d’autres séquences HTTP anormales. Une décision devient ensuite disponible pour le firewall bouncer.

Le firewall bouncer maintien des listes IPv4 et IPv6 puis les consulte depuis iptables. Une adresse décidée est rejetée avant le conteneur. C’est un blocage par adresse source : il protège les services Docker publiés concernés, pas un unique nom de domaine. Le bénéfice est concret, mais la portée doit être assumée.

La documentation officielle confirme que le firewall bouncer est une remédiation réseau et non un WAF, et qu’une application Docker publiée a besoin de la chaîne DOCKER-USER pour être couverte.

Étape 1 : vérifier la base avant d’ajouter quoi que ce soit

Je pars d’un hôte Debian avec CrowdSec et son firewall bouncer déjà installés. Avant toute modification, je contrôle les services et la configuration réellement active :

sudo systemctl is-active crowdsec
sudo systemctl is-active crowdsec-firewall-bouncer
sudo cscli collections list
sudo cscli bouncers list
sudo iptables -S INPUT
sudo iptables -S DOCKER-USER
sudo ip6tables -S INPUT
sudo ip6tables -S DOCKER-USER

Ne publiez jamais la sortie complète de la configuration du bouncer : elle contient sa clé locale. La liste des bouncers et les chaînes iptables suffisent à repérer un ancien composant ou un ordre inattendu.

Étape 2 : installer la collection et acquérir les accès NPM

Pour CrowdSec Nginx Proxy Manager, j’installe la collection maintenue par CrowdSec, puis j’ajoute une acquisition qui couvre tous les Proxy Hosts NPM. Adaptez le chemin au montage persistant de votre propre stack :

sudo cscli collections install crowdsecurity/nginx-proxy-manager
sudo nano /etc/crowdsec/acquis.d/npm-proxy-hosts.yaml
filenames:
  - /chemin/vers/npm/data/logs/proxy-host-*_access.log
labels:
  type: nginx-proxy-manager
source: file

Le wildcard est important : un nouveau Proxy Host est suivi sans créer une nouvelle règle à la main. Ici CrowdSec ne consomme que le format d’accès natif attendu par son parser.

Validez sans toucher à NPM, puis rechargez uniquement CrowdSec :

sudo crowdsec -t
sudo systemctl reload crowdsec
sudo cscli metrics show acquisition
sudo cscli metrics show parsers
sudo cscli metrics show scenarios

Étape 3 : observer avant de bloquer Docker

J’ai laissé cette collecte tourner 48 heures avant de placer CrowdSec dans le chemin des paquets Docker. Le parser officiel a traité environ 54 700 lignes NPM, avec seulement deux lignes non interprétées. Aucun incident de disponibilité n’a été signalé. C’est assez pour vérifier que la source est vivante et que les scénarios HTTP ont une matière réelle, pas assez pour prétendre éliminer tout faux positif.

Fail2ban reste en place pour les échecs de Basic Auth : son rôle est précis et complémentaire. CrowdSec couvre la détection comportementale plus large et les décisions reçues par son moteur. Empiler les deux n’a de sens que si l’ordre de leurs chaînes firewall est connu et contrôlé.

Étape 4 : raccorder la remédiation à INPUT et DOCKER-USER

Le piège Docker est classique. Une connexion vers un port publié est traduite puis transférée vers le conteneur. Elle passe donc par FORWARD et DOCKER-USER, pas seulement par INPUT. Limiter CrowdSec à INPUT protège SSH et l’hôte, mais laisse passer le trafic web publié par Docker.

Dans la configuration du firewall bouncer, je garde les deux points d’entrée :

iptables_chains:
  - INPUT
  - DOCKER-USER

Le mode géré du bouncer crée ses ipsets et sa chaîne CrowdSec. Après le redémarrage du seul bouncer, il place cette chaîne en première position dans INPUT et DOCKER-USER, en IPv4 comme en IPv6. Cette priorité est volontaire : une décision CrowdSec est évaluée avant les autres mécanismes dynamiques. Vérifiez toujours l’ordre observé, ne le déduisez pas d’un fichier YAML.

sudo systemctl restart crowdsec-firewall-bouncer
sudo iptables -S INPUT
sudo iptables -S DOCKER-USER
sudo ip6tables -S INPUT
sudo ip6tables -S DOCKER-USER
sudo cscli metrics show bouncers

Étape 5 : déployer comme un changement firewall, avec rollback

Modifier une chaîne de filtrage en production demande un retour arrière ciblé. Avant le changement, j’ai sauvegardé la configuration du bouncer et les règles IPv4 et IPv6, puis armé un rollback automatique de cinq minutes. Il est annulé seulement après les contrôles. Gardez une seconde session SSH ouverte durant toute la manipulation.

sudo install -d -m 0700 /var/backups/crowdsec/test
sudo cp -p /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml   /var/backups/crowdsec/test/
sudo iptables-save -f /var/backups/crowdsec/test/iptables.v4
sudo ip6tables-save -f /var/backups/crowdsec/test/iptables.v6

Le rollback doit restaurer la configuration précédente du bouncer, redémarrer ce seul service, puis vérifier les chaînes. Je préfère cela à une restauration aveugle de tout le firewall : Docker, Fail2ban et les autres protections dynamiques restent ainsi sous le contrôle de leurs services respectifs.

Étape 6 : prouver le chemin de blocage sans viser une vraie adresse

Le test utile consiste à créer brièvement des décisions de blocage, une IPv4 et une IPv6 réservées aux exemples, puis à vérifier leur présence dans les ipsets et la règle CrowdSec dans les deux chaînes. Il faut supprimer les décisions immédiatement après le test.

sudo cscli decisions add --ip 192.0.2.1 --duration 2m --reason test-documentation
sudo cscli decisions add --ip 2001:db8::1 --duration 2m --reason test-documentation

sudo iptables -S DOCKER-USER
sudo ip6tables -S DOCKER-USER
sudo cscli decisions list

sudo cscli decisions delete --ip 192.0.2.1
sudo cscli decisions delete --ip 2001:db8::1

Ensuite, contrôlez les réponses HTTP de vos hôtes publics et SSH depuis une autre session. Dans mon cas, les réponses attendues sont restées en place après le changement : redirection du site public, refus Basic Auth des applications protégées et accès SSH normal.

Ce que CrowdSec protège, et ce qu’il ne protège pas

  • Oui : les IP décidées par CrowdSec sont rejetées avant d’arriver sur NPM et les conteneurs concernés.
  • Oui : IPv4 et IPv6 sont suivies lorsque les deux familles sont activées.
  • Non : ce montage n’inspecte pas chaque requête HTTP comme un WAF applicatif.
  • Non : une décision IP n’est pas limitée à un seul Proxy Host.
  • À revoir : chaque futur port Docker publié doit être évalué, car la chaine DOCKER-USER a une portée transversale.

Le point de vigilance avec le plan Community

Mon relevé de la Console du 20 août à 10 h 09 affichait 102 alertes sur les dernières 24 heures et un composant de remédiation actif. C’est un bon signal de vie, pas un compteur de blocages réussis : une alerte décrit une détection, alors que le firewall bouncer applique ensuite les décisions compatibles.

Mais après le déploiement, la Console CrowdSec a atteint son quota mensuel de 500 alertes en moins de 7j… Cela arrête l’enregistrement des nouvelles alertes dans la Console jusqu’au renouvellement du quota, mais ne désactive ni le Security Engine ni les décisions déjà actives, et le firewall bouncer continue son travail. C’est une distinction importante : la visibilité distante peut être limitée alors que la protection locale reste active.

La leçon n’est pas de prendre un abonnement dans la minute. C’est de prévoir un suivi local des métriques du moteur et du bouncer, puis de décider calmement si le quota Community est compatible avec le volume réel. Une sécurité invisible est difficile à piloter, même quand elle bloque correctement. Dans le cas de mon blog, je me m’en fiche un peu d’avoir toutes les alertes dans la console, ce n’est qu’un « personal lab ». En revanche, je suis content de contribuer aux données envoyées à CrowdSec, ils ont une approche communautaire depuis le début sur laquelle je suis un fervent believer en Cyber. Et cerise sur le gâteau, c’est des petits français, donc geekeries.org contribue à à l’économie cyber souveraine, profitez en 🙂 !

Conclusion

Pour terminer sur cette intégration, mon CrowdSec pour Nginx Proxy Manager reste volontairement sobre : image officielle conservée, acquisition côté hôte, collection officielle et blocage IP dans INPUT et DOCKER-USER. Les 48 heures d’observation, le test avec adresses de documentation et le rollback temporisé ont permis de passer du mode détection au blocage sans casser le reverse proxy. Mais c’est ce qui assure la bonne maintenance du site dans le temps (rappelez vous, tout ce qui est développement ou configuration « custom » est la promesse d’un incident ou de travail futur)

Donc ce n’est pas un WAF magique, mais c’est très bien ainsi. Le montage est lisible, réversible et s’insère dans le firewall déjà en place. Pour un serveur personnel exposé, c’est exactement le niveau de sophistication que je recherchais. Et si vous ne connaissiez pas Crowdsec, c’est une bonne solution de Geek 😉 !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. Découvrez comment les données de vos commentaires sont traitées.