Demande : « ne pas avoir le basic auth Traefik quand on est sur le Wi-Fi partagé avec le homelab ». La réponse intuitive — une règle `ClientIP(192.168.1.0/24)` — ne peut pas marcher, et la variante « je regarde CF-Connecting-IP » est un contournement d'authentification. Ce commit écrit la mesure pour qu'on ne re-découvre ni l'une ni l'autre. Mesuré (2026-07-28, homelab) : - `kadans.arcodange.fr` résout vers Cloudflare MÊME depuis le LAN, et redescend par le tunnel cloudflared. L'adresse de socket vue par Traefik est donc toujours celle d'un pod, en 10.42.x.x. - Traefik SAIT qui est le vrai client (journal d'accès : l'IPv6 de la maison, `2a01:cb04:dff:cf00::/64` — pas l'IPv4 qu'`ipify` injecte dans `localIp`), car l'entrypoint `web` fait confiance aux en-têtes venant de 10.42.0.0/16. - Mais le matcher de routeur `ClientIP()`, lui, juge la SOCKET. Prouvé par trois routeurs temporaires vers un Service sans endpoint (503 = règle matchée, 401 = repli sur le routeur normal) : témoin 503, `ClientIP(<IPv6 maison>)` 401, `ClientIP(10.42.0.0/16)` 503. Sondes retirées après mesure. D'où deux pièges, écrits là où on les rencontrerait : - `localIp@file` sur un routeur `.fr` laisserait entrer Internet entier, parce que `10.42.0.0/16` est dans son `sourceRange` et qu'`ipAllowList` juge lui aussi la socket. Vérifié : aucun routeur `.fr` ne le porte aujourd'hui — le piège est latent, et les cinq endroits d'où on pourrait le recopier (2 gabarits `.fr`, 3 values `.lab`) portent désormais l'avertissement. - Un en-tête posé par le client ne peut pas piloter une exemption d'auth : Cloudflare ne retire pas les en-têtes inconnus, et Traefik reste joignable en direct sur 192.168.1.201. Voie retenue, et pourquoi celle-là : la décision « suis-je à la maison ? » doit être prise là où la vraie IP est native et non falsifiable, donc au bord, chez Cloudflare. Une règle de transformation y pré-remplit l'en-tête `Authorization` pour les IP du foyer. Traefik ne bouge pas, aucun certificat public à produire, aucun changement DNS. Surface d'attaque ajoutée : AUCUNE — `kadans:kkadans` est déjà en clair dans le chart de kadans, Cloudflare ne fait que le taper à notre place. Et quand l'IP du foyer dérive, la règle cesse de matcher : le navigateur redemande le mot de passe. Dégradation douce, pas de panne. Le doc écrit l'action Cloudflare mot pour mot (expression, en-tête, valeur, et l'équivalent Terraform pour `cms/cloudflare`) plutôt que de la supposer faite, avec les deux commandes qui la vérifient — et nomme le repli si Cloudflare refuse de modifier `Authorization` : Cloudflare Access avec une politique de bypass, PAS un en-tête secret. Enfin, un contre-exemple utile : le bouncer CrowdSec, lui, juge la VRAIE IP (journal à l'appui). Son `clientTrustedIPs` contient `10.42.0.0/16` sans que ce soit un trou — noté pour que personne ne « corrige » cette ligne en croyant y reproduire le piège. ⚠ Aucun des réglages en cause ne vit dans ce dépôt : les valeurs Helm de Traefik, `dynamic.yaml`/`localIp` et le Middleware crowdsec sont dans `factory` (ansible), le tunnel et le DNS dans `cms`. Ce dépôt reçoit la mesure et les garde-fous parce que c'est lui qui héberge les gabarits `.fr` et les consommateurs de `localIp@file`. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GdUCA5Uz8QyMwa2P4Pg2hK
11 KiB
Ce que Traefik voit du client — et pourquoi « pas de mot de passe à la maison » ne se règle pas dans Traefik
Mesuré le 2026-07-28 sur le homelab (kubectl --context=default, k3s pi1/pi2/pi3).
Ce document existe parce que la question « peut-on sauter le basic auth quand on est
sur le Wi-Fi de la maison ? » a une réponse contre-intuitive, et que la réponse
intuitive est un trou de sécurité.
1. Le chemin réel d'une requête .fr
navigateur (maison)
└─ DNS : kadans.arcodange.fr → 172.67.130.8 / 104.21.3.16 (Cloudflare, PAS le LAN)
└─ edge Cloudflare (proxy orange)
└─ tunnel cloudflared (sortant, pod du cluster, CIDR 10.42.0.0/16)
└─ traefik.kube-system.svc:80 (entrypoint `web`, pas de TLS ici)
└─ crowdsec → basic auth → service
Le tunnel est sortant : il n'y a aucune redirection de port, aucun chemin
depuis Internet vers Traefik autre que Cloudflare. Le nom .fr est un CNAME
proxifié vers <tunnel-id>.cfargotunnel.com (cms/cloudflare, module
cloudflared_tunnel, mapping unique *.arcodange.fr).
Conséquence n°1 : même depuis le salon, la requête sort de la maison, fait un
aller-retour par Cloudflare, et revient par le tunnel. L'adresse de la socket
que Traefik accepte est toujours celle d'un pod cloudflared en 10.42.x.x.
Le .lab, lui, va droit au but : kadans.arcodange.lab → 192.168.1.201.
2. Deux notions d'« IP client », et elles ne coïncident pas
L'entrypoint web fait confiance aux en-têtes transmis depuis le CIDR des pods
(ports.web.forwardedHeaders.trustedIPs: ["10.42.0.0/16"], playbook
factory/.../playbooks/system/k3s_config.yml). Traefik sait donc qui est le
vrai client — le journal d'accès le prouve, sur une requête émise depuis la maison :
2a01:cb04:dff:cf00:f9d7:4026:9ffc:b042 - kadans [28/Jul/2026:14:22:49 +0200]
"GET /?sonde=… HTTP/1.1" 200 … "kadans-kadans-public-kadans-arcodange-fr@kubernetes"
Deux choses à retenir de cette ligne :
- Traefik résout le vrai client depuis
X-Forwarded-For; - la maison sort en IPv6 (
2a01:cb04:dff:cf00::/64), pas en IPv4. Le86.238.234.54/32que le playbook injecte danslocalIpviaipifyn'est qu'une des deux adresses publiques du foyer, et pas celle qu'un navigateur moderne utilise en priorité.
Mais le matcher de routeur ClientIP() n'utilise PAS cette résolution. Il
juge l'adresse de la socket. Mesuré, pas supposé — trois routeurs temporaires
posés sur kadans.arcodange.fr, pointant vers un Service sans endpoint (donc
503 si la règle matche, et repli sur le routeur normal — 401 — sinon) :
| sonde | règle ajoutée à Host(kadans.arcodange.fr) |
attendu si… | mesuré |
|---|---|---|---|
| T | (témoin, aucune contrainte d'IP) | le dispositif fonctionne → 503 | 503 |
| A | ClientIP("2a01:cb04:dff:cf00::/64") |
ClientIP lit X-Forwarded-For |
401 |
| B | ClientIP("10.42.0.0/16") |
ClientIP lit la socket |
503 |
Le témoin valide le montage ; A échoue, B réussit. ClientIP() voit le pod
cloudflared, jamais le vrai client. (Sondes supprimées après mesure.)
3. Les deux pièges qui en découlent
Piège A — localIp@file ne doit JAMAIS toucher un routeur .fr
localIp est un ipAllowList dont le sourceRange contient 10.42.0.0/16
(le CIDR des pods). Comme ipAllowList juge lui aussi l'adresse de la socket
par défaut, l'attacher à un hôte .fr laisserait passer Internet entier :
toute requête arrivant par le tunnel se présente en 10.42.x.x, donc « locale ».
État vérifié le 2026-07-28 : aucun routeur .fr ne porte localIp@file. Le
piège est latent, pas ouvert. Il le reste tant que personne ne recopie
localIp@file depuis un values .lab (grafana, vault, plausible) vers un
ingress-public.yaml. Les deux gabarits .fr de ce dépôt portent un
avertissement à cet endroit précis.
Piège B — se fier à un en-tête est un contournement d'authentification
« Sauter le basic auth si CF-Connecting-IP est celle de la maison » (ou si un
en-tête maison est présent) fait reposer une exemption d'authentification sur
une valeur que le client écrit lui-même. Cloudflare ne retire pas les en-têtes
inconnus : n'importe qui sur Internet peut les envoyer. Et Traefik reste joignable
en direct sur le LAN (192.168.1.201:80). On ne prend pas cette voie.
4. Pourquoi ce n'est pas réparable dans Traefik
Traefik enchaîne ses middlewares inconditionnellement : il n'existe pas de
« basic auth sauf si … ». Le seul moyen de sauter un middleware est de faire
matcher un autre routeur — et le seul discriminant d'IP au niveau routeur est
ClientIP(), qui, on vient de le mesurer, ne voit que le tunnel. Un
ipAllowList avec ipStrategy.depth verrait bien la vraie IP, mais un
ipAllowList bloque ; il ne sait pas « laisser entrer sans mot de passe ».
Faire pointer le LAN vers Traefik (DNS à double horizon, ou enregistrement
Cloudflare « DNS only ») rendrait ClientIP(192.168.1.0/24) honnête — mais
coûterait, en plus du DNS : un certificat publiquement valide pour
kadans.arcodange.fr sur Traefik (aujourd'hui il sert la CA interne,
CN=arcodange.lab ; un curl --resolve … 192.168.1.201 échoue au TLS, code
000), donc un nouveau certResolver DNS-01 Cloudflare et le jeton qui va avec,
plus un routeur websecure pour un hôte qui vit aujourd'hui en web clair.
Beaucoup, pour du confort. Et « DNS only » retirerait en prime le bouclier
Cloudflare de cet hôte.
La décision doit donc être prise là où la vraie IP est native et non
falsifiable : au bord, chez Cloudflare (ip.src).
5. La voie retenue — Cloudflare tape le mot de passe à ta place
Le basic auth de kadans.arcodange.fr est un garde-fou assumé, pas un
secret : kadans:kkadans, et son hash bcrypt est en clair dans
chart/values.yaml du dépôt kadans. C'est ce fait qui rend la solution
simple et sans coût de sécurité :
Une règle Cloudflare pré-remplit l'en-tête
Authorizationpour les requêtes venant du réseau de la maison. Traefik ne change pas, le mot de passe reste exigé partout ailleurs. Cloudflare ne fait que le taper à ta place.
Surface d'attaque ajoutée : aucune. Un attaquant qui forge cet en-tête
obtient exactement ce que curl -u kadans:kkadans lui donne déjà aujourd'hui.
C'est la différence de fond avec le piège B : ici on ne crée pas d'exemption, on
automatise une saisie.
Et quand l'IP du foyer change (elle est dynamique), la règle cesse simplement de matcher : le navigateur redemande le mot de passe. Dégradation douce, jamais de panne.
Action manuelle du fondateur (à faire une fois)
Cloudflare → zone arcodange.fr → Rules → Transform Rules → Modify
Request Header → Create rule :
-
Nom :
kadans — confort LAN : Authorization pré-remplie depuis la maison -
When incoming requests match (éditeur d'expression) :
(http.host eq "kadans.arcodange.fr" and ip.src in {86.238.234.54 2a01:cb04:dff:cf00::/64}) -
Then → Set static :
- Header name :
Authorization - Value :
Basic a2FkYW5zOmtrYWRhbnM=(base64 dekadans:kkadans)
- Header name :
Équivalent Terraform, si on préfère le déclarer dans cms/cloudflare :
resource "cloudflare_ruleset" "confort_lan_kadans" {
zone_id = cloudflare_zone.arcodange_fr.id
name = "confort LAN"
kind = "zone"
phase = "http_request_late_transform"
rules = [{
expression = "(http.host eq \"kadans.arcodange.fr\" and ip.src in {86.238.234.54 2a01:cb04:dff:cf00::/64})"
description = "Depuis la maison, Cloudflare tape le garde-fou basic auth a notre place"
action = "rewrite"
action_parameters = {
headers = {
"Authorization" = { operation = "set", value = "Basic a2FkYW5zOmtrYWRhbnM=" }
}
}
}]
}
Vérifier que ça marche
# depuis la maison — attendu : 200, SANS -u
curl -s -o /dev/null -w '%{http_code}\n' https://kadans.arcodange.fr/
# depuis l'extérieur (partage de connexion du téléphone) — attendu : 401
curl -s -o /dev/null -w '%{http_code}\n' https://kadans.arcodange.fr/
Ce qu'il faudra surveiller
- Les deux adresses du foyer sont dynamiques. Le préfixe IPv6 relevé le
2026-07-28 est
2a01:cb04:dff:cf00::/64et l'IPv486.238.234.54— la même que celle qu'ipifyinjecte danslocalIpà chaque passage du playbookk3s_config.yml. S'il dérive, élargir en/56avant de soupçonner Cloudflare. - ⚠ Non vérifié en vrai : que Cloudflare accepte
Authorizationcomme en-tête de requête modifiable (certains en-têtes sont réservés). Si l'éditeur refuse la règle, le repli est Cloudflare Access avec une politique Bypass sur les mêmes IP et une politique Allow par e-mail ailleurs — et le basic auth de Traefik est alors retiré. Le repli n'est pas un en-tête secret (piège B).
La version encore plus simple, si le confort ne vaut pas la règle
Le mot de passe est public. Ce qu'il achète réellement, c'est « le produit en
chantier n'est pas indexé ni tombé dessus par hasard ». Un robots.txt +
X-Robots-Tag: noindex fait ce travail-là sans mot de passe du tout. Retirer le
garde-fou est une décision produit, pas une décision d'infra : elle n'est pas
prise ici, seulement nommée.
6. Ce qui a été vérifié et qui va bien
Le bouncer CrowdSec, lui, juge la vraie IP — contrairement à ClientIP().
Journal Traefik sur une requête de la maison et sur des robots :
CrowdsecBouncerTraefikPlugin: ServeHTTP ip:2a01:cb04:dff:cf00:f9d7:4026:9ffc:b042 isTrusted:false
CrowdsecBouncerTraefikPlugin: ServeHTTP ip:144.24.58.222 isTrusted:false
Son clientTrustedIPs contient pourtant 10.42.0.0/16 : ce n'est pas un
trou, parce que le plugin ne compare pas cette liste à l'adresse de la socket
mais à l'IP résolue. La convention .fr (« crowdsec devant tout ») tient donc
ce qu'elle promet. Écrit ici pour que personne ne « corrige » cette ligne en
croyant reproduire le piège A.
Où vit quoi (aucun de ces réglages n'est dans ce dépôt — vérifié) :
| réglage | dépôt / fichier |
|---|---|
valeurs Helm Traefik, dynamic.yaml, localIp |
factory — ansible/arcodange/factory/playbooks/system/k3s_config.yml |
Middleware crowdsec@kube-system |
factory — .../playbooks/tools/roles/crowdsec/tasks/main.yml |
tunnel + DNS *.arcodange.fr |
cms — cloudflare/iac.tf, cloudflare/modules/cloudflared_tunnel/ |
basic auth .fr de kadans |
kadans — chart/values.yaml (public.basicAuth) |
gabarits d'exposition .fr de ce dépôt |
grafana/templates/ingress-public.yaml, minio/templates/ingress-public.yaml |