Fail2ban devant Nginx Proxy Manager : bloquer les attaques sans casser Docker

Une licorne Geekeries protège Nginx Proxy Manager avec un bouclier Fail2ban devant des requêtes rejetées

Salut les Geeks ! Après avoir placé mon VS Code Web derrière une Basic Auth, il restait un détail : un attaquant pouvait toujours essayer des mots de passe toute la journée sur la basic auth et chopper un accès root au serveur. J’ai donc configuré Fail2ban avec Nginx Proxy Manager, sans modifier l’image Docker de NPM et sans ajouter un conteneur privilégié de plus. La recette paraît courte, mais Docker cache un petit piège dans ses chaînes iptables.

Ce tutoriel part d’un Nginx Proxy Manager installé avec Docker Compose, dont le dossier /data est persistant sur l’hôte. Adaptez les chemins et l’identifiant du Proxy Host à votre installation. Surtout, gardez une seconde session SSH ouverte pendant les essais. Du coup on va dire que cet article est un Spin-off de la la série « Docker et Portainer », pour rappel

  1. Les conteneurs pour les débutants
  2. Créer une stack vsftpd mono-image
  3. Déployer une stack Docker Guacamole
  4. Docker, OpenVPN et Splunk
  5. Customiser un conteneur PHP-FPM
  6. Déployer Nextcloud avec Docker
  7. Mettre à jour Portainer et ses conteneurs
  8. Déployer Jitsi Meet avec Docker
  9. Superviser Docker avec Splunk
  10. Installer TheHive et Cortex avec Docker
  11. Installer OpenCTI avec Docker
  12. MariaDB, quand latest casse les sauvegardes

Ce que nous allons protéger

Le principe est simple : Nginx Proxy Manager écrit une réponse HTTP 401 lorsqu’une authentification Basic échoue. Fail2ban lit le journal du Proxy Host, compte les refus par adresse IP, puis bloque temporairement la source sur les ports 80 et 443.

Je reste volontairement dans une architecture KISS :

  • l’image officielle Nginx Proxy Manager reste intacte ;
  • le journal natif et persistant de NPM sert de source ;
  • Fail2ban tourne sur l’hôte, où il peut déjà administrer le firewall ;
  • le premier périmètre est un seul Proxy Host protégé par Basic Auth.

Si Fail2ban est déjà installé pour SSH, ne le réinstallez pas. Vérifiez d’abord son état :

sudo fail2ban-client status
sudo systemctl status fail2ban --no-pager

Dans mon cas, la jail sshd était déjà active. Le travail consistait uniquement à ajouter un filtre, une action et une jail.

Étape 1 : retrouver le bon journal NPM

Commencez par identifier votre montage persistant de NPM (Remplacez le nom du conteneur si nécessaire) :

NPM_CONTAINER=nginx-proxy-manager
docker inspect "$NPM_CONTAINER" --format '{{range .Mounts}}{{println .Source " -> " .Destination}}{{end}}'
docker inspect "$NPM_CONTAINER" --format '{{json .NetworkSettings.Ports}}'

Les journaux se trouvent généralement dans le sous-dossier logs du volume monté sur /data. Chaque Proxy Host possède un identifiant. On peut le retrouver dans l’interface NPM, puis confirmer le nom de domaine dans sa configuration :

NPM_DATA=/srv/npm/data
PROXY_ID=9

sudo grep -R "server_name" "$NPM_DATA/nginx/proxy_host/$PROXY_ID.conf"
sudo ls -l "$NPM_DATA/logs/proxy-host-${PROXY_ID}_access.log"

Provoquez ensuite un seul échec d’authentification depuis une connexion de test et contrôlez les dernières lignes :

sudo tail -n 20 "$NPM_DATA/logs/proxy-host-${PROXY_ID}_access.log"

La ligne utile doit contenir le statut 401 et l’adresse réelle dans le champ fixe [Client …]. Si ce n’est pas le cas, arrêtez-vous : le filtre ci-dessous ne correspond pas à votre format.

Étape 2 : créer un filtre strict

Créez /etc/fail2ban/filter.d/npm-basic-auth.conf :

[Definition]
failregex = ^.*?\s+-\s+-\s+401\s+-\s+\S+\s+\S+\s+\S+\s+"[^"]*"\s+\[Client <HOST>\]\s+.*$
ignoreregex =

Le placement de <HOST> est important. Une URL, un User-Agent ou un Referer sont contrôlables par le client. Capturer une adresse au mauvais endroit permettrait à quelqu’un de faire bannir une victime à sa place.

Testez le filtre sur le vrai journal avant de l’activer :

sudo fail2ban-regex \
  "$NPM_DATA/logs/proxy-host-${PROXY_ID}_access.log" \
  /etc/fail2ban/filter.d/npm-basic-auth.conf

Vérifiez les correspondances, mais aussi les lignes ignorées. Des réponses 200, 301, 403 ou 500 ne doivent pas déclencher ce filtre. La documentation officielle détaille les précautions à prendre pour écrire des filtres Fail2ban sûrs.

Étape 3 : comprendre le piège iptables de Docker

Sur un serveur classique, on pense naturellement à bannir dans la chaîne INPUT. Pourtant, les ports publiés par Docker passent généralement par FORWARD et les chaînes Docker. La documentation Docker recommande la chaîne DOCKER-USER pour les règles ajoutées par l’administrateur.

sudo iptables -S INPUT
sudo iptables -S DOCKER-USER
sudo ip6tables -S INPUT
sudo ip6tables -S DOCKER-USER

Sur mon serveur, le trafic IPv4 publié par Docker traverse DOCKER-USER, tandis que le chemin IPv6 actuellement assuré par docker-proxy doit aussi être lui couvert dans INPUT (mais sur ip6tables du coup). Une seule action aurait donc laissé un angle mort.

Ne recopiez pas aveuglément cette topologie. Observez les règles de votre hôte, puis testez IPv4 et IPv6 séparément. Docker, nftables et les versions récentes du moteur peuvent faire évoluer le chemin réel des paquets.

Étape 4 : préserver les règles déjà prioritaires

Mon premier emplacement dans DOCKER-USER est réservé à une règle AbuseIPDB custom. Or l’action iptables standard de Fail2ban s’insère par défaut en tête. J’ai donc créé une variante minimale qui l’insère en position 2. Si vous n’avez aucune règle dont l’ordre doit être préservé, utilisez l’action standard iptables-multiport et sautez cette étape. Sinon, créez /etc/fail2ban/action.d/iptables-multiport-after-first.conf :

[INCLUDES]
before = iptables.conf

[Definition]
type = multiport

_ipt_add_rules = <_ipt_for_proto-iter>
              { %(_ipt_check_rule)s >/dev/null 2>&1; } || { <iptables> -I <chain> 2 %(_ipt_chain_rule)s; }
              <_ipt_for_proto-done>

Cette surcharge reste volontairement minuscule : tout le reste provient de l’action fournie par la version installée de Fail2ban. Après chaque mise à jour majeure du firewall, il faudra comparer ce fragment avec le fichier iptables.conf officiel de Fail2ban.

Étape 5 : activer la jail Fail2ban Nginx Proxy Manager

Créez /etc/fail2ban/jail.d/npm-basic-auth.local en adaptant le chemin et l’identifiant du Proxy Host :

[npm-basic-auth]
enabled  = true
filter   = npm-basic-auth
logpath  = /srv/npm/data/logs/proxy-host-9_access.log
backend  = auto
port     = http,https
protocol = tcp
maxretry = 8
findtime = 10m
bantime  = 30m

action = iptables-multiport-after-first[name=npm-basic-auth-forward, port="http,https", protocol=tcp, chain=DOCKER-USER]
         iptables-multiport[name=npm-basic-auth-input, port="http,https", protocol=tcp, chain=INPUT]

Huit erreurs en dix minutes, suivies d’un ban de trente minutes, constituent un départ raisonnable pour vos tests. Ce ne sont pas des valeurs universelles. Une famille, un bureau partagé ou un VPN NAT-é peuvent concentrer beaucoup d’utilisateurs derrière la même adresse. Notez d’ailleurs que certains applications web (Splunk, VSCode) peuvent générer « naturellement » des 401 au cours de leurs fonctionnement (sans que cela vous bloque), soyez prudent :). Enfin bref, j’ai redurci un peu la conf :

JailPar défautMaintenant
NPM8 essais/10 min, ban 30 min5 essais/15 min, ban 12 h
SSH3 essais/4 h, ban 7 jours2 essais/4 h, ban 14 jours

Autre conséquence à connaître : ces actions bloquent la source sur 80 et 443 au niveau de l’hôte. Le ban ne concerne donc pas uniquement le nom de domaine surveillé, mais tous les sites publiés sur ces ports. C’est efficace, mais cela mérite une décision consciente. Le principe des bans exponentiels reste désactivés pour limiter le risque de verrouillage prolongé en ligne avec les recommandation de limitation des tentatives recommandé par OWASP et le NIST, avec les paramètres officiellement prévus par Fail2ban.

Étape 6 : valider avant de recharger

Fail2ban sait contrôler toute sa configuration sans la rendre active :

sudo fail2ban-client -t

La commande doit se terminer sans erreur. Rechargez ensuite le service existant, sans redémarrer Docker, NPM ou le réseau :

sudo fail2ban-client reload
sudo fail2ban-client status
sudo fail2ban-client status npm-basic-auth
sudo fail2ban-client status sshd

Contrôlez également la présence et l’ordre des sauts créés :

sudo iptables -S DOCKER-USER
sudo iptables -S INPUT
sudo ip6tables -S INPUT

La jail SSH doit toujours être active. C’est une vérification bête, donc précisément le genre de vérification que j’aime faire avant de fermer ma seconde session.

Étape 7 : tester sans se bannir soi-même

Avant un vrai test, on peut vérifier les actions avec des adresses réservées à la documentation. Elles ne doivent correspondre à aucun client réel :

sudo fail2ban-client set npm-basic-auth banip 192.0.2.10
sudo fail2ban-client status npm-basic-auth
sudo iptables -S f2b-npm-basic-auth-forward

sudo fail2ban-client set npm-basic-auth unbanip 192.0.2.10

sudo fail2ban-client set npm-basic-auth banip 2001:db8::10
sudo ip6tables -S f2b-npm-basic-auth-input
sudo fail2ban-client set npm-basic-auth unbanip 2001:db8::10

Pour le test de bout en bout, utilisez une connexion distincte de votre accès SSH, par exemple un téléphone en 4G/5G ou un VPN. Provoquez le nombre prévu d’échecs Basic Auth, vérifiez le ban, puis débannissez l’adresse de test :

sudo fail2ban-client status npm-basic-auth
sudo fail2ban-client set npm-basic-auth unbanip ADRESSE_IP_DE_TEST

J’ai ensuite laissé tourner la configuration. Après trente minutes, NPM n’avait subi aucun redémarrage, sa configuration Nginx était valide, aucun 5xx n’apparaissait dans le nouveau journal quotidien et les jails sshd et npm-basic-auth restaient actives. Aucun client réel n’était banni.

Retour arrière propre

Le rollback doit viser uniquement cette jail. Inutile de redémarrer Docker ou de bricoler toutes les règles à la main :

sudo fail2ban-client stop npm-basic-auth
sudo iptables -S DOCKER-USER
sudo iptables -S INPUT
sudo ip6tables -S INPUT

Déplacez ensuite la jail hors de jail.d, restaurez vos fichiers sauvegardés si nécessaire, puis contrôlez et rechargez :

sudo mv /etc/fail2ban/jail.d/npm-basic-auth.local \
  /etc/fail2ban/npm-basic-auth.local.disabled
sudo fail2ban-client -t
sudo fail2ban-client reload
sudo fail2ban-client status sshd

Vérifiez enfin HTTPS, les WebSockets de l’application protégée et SSH. Dans mon installation, la configuration Fail2ban, les données persistantes de NPM et les unités utiles font aussi partie de la sauvegarde du serveur. Les journaux, eux, suivent leur propre politique de rotation pour ne pas gonfler les archives.

Ce que Fail2ban Nginx Proxy Manager améliore, et ce qu’il ne remplace pas

Fail2ban réduit efficacement le bruit des attaques répétées contre une Basic Auth, et arrête les brutes force simple mais pas ceux réparti (coucou openbullet). Il ne transforme pas pour autant un mot de passe faible en fort, ni ne remplace pas la limitation réseau ou ne fournit pas de MFA. Si l’accès peut être limité à un VPN ou à quelques adresses, cette restriction reste encore plus simple et plus robuste.

Ce montage prolonge mon ancien focus sur Fail2ban, avec le détail qui change tout ici : les ports sont publiés par Docker. Une fois le vrai chemin des paquets identifié, la solution tient dans trois petits fichiers, se teste sans couper le proxy et se retire proprement. C’est exactement le niveau de sophistication que j’attends d’un garde-barrière 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. En savoir plus sur la façon dont les données de vos commentaires sont traitées.