Chaîne reverse proxy — note d'architecture¶
Le chemin complet¶
flowchart TB
subgraph internet["Internet"]
client["Client<br/>navigateur"]
end
subgraph ovh["OVH"]
dns["Zone DNS savethemall.eu<br/>A → <IP publique>"]
end
subgraph maison["LAN <sous-reseau LAN>"]
velop["Routeur Velop<br/>NAT 443 → nas:8080"]
subgraph nas["NAS Synology · le NAS"]
nascaddy["nas-caddy — EDGE WAN<br/>TLS explicite, certs copiés"]
statique["Sites statiques locaux<br/>weber · chroniques · rgpd"]
end
subgraph vmd["vm-docker · vm-docker"]
caddy["Caddy<br/>routage interne + CrowdSec"]
authentik["Authentik<br/>outpost embarqué"]
app["Conteneur applicatif<br/>infrastructure_network"]
end
end
client -->|"résout le nom"| dns
client -->|"HTTPS 443"| velop
velop --> nascaddy
nascaddy -->|"local"| statique
nascaddy -->|"import to_vmdocker<br/>header_up Host"| caddy
caddy -->|"forward_auth"| authentik
authentik -.->|"302 vers auth.savethemall.eu<br/>si pas de session"| client
caddy -->|"reverse_proxy nom:port"| app
Pourquoi deux étages¶
Un seul aurait suffi fonctionnellement. Il y en a deux parce que les deux machines ne savent pas faire la même chose :
- Le NAS est le seul à voir le WAN. Le NAT du routeur pointe sur lui. Il porte donc les sites statiques qui doivent survivre à la mort de vm-docker.
- vm-docker est le seul à pouvoir bloquer. Le NAS tourne sur un noyau DSM sans
xt_set, sans arbre/lib/modules, etsudoy exige un mot de passe : aucun blocage L3 n'y est possible. La remédiation CrowdSec vit donc sur vm-docker, en 403 applicatif.
Les deux conséquences qui coûtent des heures¶
Un filtrage L3 posé sur vm-docker ne bloque rien
Toutes les requêtes WAN arrivent sur vm-docker avec nas comme source.
Une règle par IP client y est structurellement aveugle.
Ce qui fonctionne : trusted_proxies static le NAS/32 dans les options
globales de Caddy, qui lui fait lire le X-Forwarded-For de nas-caddy. Les
modules qui consomment l'IP client — CrowdSec compris — voient alors la vraie.
Les statiques servis par le NAS ne passent pas par CrowdSec
Ils ne traversent jamais le Caddy de vm-docker. La remédiation L7 ne les couvre pas. C'est une limite assumée, pas un oubli.
L'authentification¶
Le snippet partagé, dans le Caddyfile de vm-docker :
(authentik) {
forward_auth authentik-server:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-authentik-username X-authentik-groups X-authentik-email X-authentik-name
}
}
Et son équivalent au vrai edge, pour les statiques servis localement par le NAS :
(authentik_edge) {
forward_auth vm-docker:9092 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-authentik-username X-authentik-groups X-authentik-email X-authentik-name
}
}
Ce que forward_auth fait, requête par requête¶
sequenceDiagram
autonumber
participant N as Navigateur
participant C as Caddy
participant A as Authentik
participant S as Service
N->>C: GET /une-page
C->>A: sous-requête /outpost.goauthentik.io/auth/caddy
alt pas de session
A-->>C: 401
C-->>N: 302 vers auth.savethemall.eu
N->>A: connexion
A-->>N: 302 vers /outpost.goauthentik.io/callback
Note over N,C: c'est CETTE route qui doit exister<br/>dans un `handle` frère, sinon le callback<br/>tombe dans le reverse_proxy de l'app
N->>C: GET /outpost.goauthentik.io/callback
C->>A: relais vers authentik-server:9000
A-->>N: cookie de session, puis 302 vers /une-page
N->>C: GET /une-page (avec le cookie)
end
C->>A: sous-requête d'autorisation
A-->>C: 200 + en-têtes X-authentik-*
C->>S: reverse_proxy, en-têtes recopiés
S-->>N: la page
Le service ne voit jamais le mot de passe : il reçoit une requête déjà autorisée, avec l'identité en en-têtes. C'est aussi sa limite — il n'a aucun moyen de savoir qu'une session s'est terminée.
forward_auth passe avant handle¶
L'ordre d'exécution des directives Caddy n'est pas l'ordre du fichier.
forward_auth est ordonné avant handle et reverse_proxy : il attrape la
requête en premier, quoi qu'on écrive après lui — dans le même bloc comme dans un
snippet importé.
flowchart TB
req["Requête<br/>/outpost.goauthentik.io/callback"]
subgraph fichier["Ce que le fichier donne à lire"]
f1["1. handle /outpost…/*"]
f2["2. forward_auth"]
f3["3. reverse_proxy app"]
end
subgraph reel["L'ordre réellement appliqué par Caddy"]
r1["1. forward_auth"]
r2["2. handle"]
r3["3. reverse_proxy"]
end
req --> fichier
fichier -.->|"n'est PAS<br/>l'ordre d'exécution"| reel
r1 --> perdu["forward_auth intercepte<br/>→ 302, boucle de connexion"]
classDef bad stroke-width:3px,stroke-dasharray: 5 5
class perdu bad
Conséquence : la route de l'outpost ne peut pas être posée une fois pour toutes
dans le snippet (authentik). Les deux écritures suivantes ont été essayées, elles
passent caddy validate, se rechargent sans erreur, et ne changent rien :
(authentik) {
handle /outpost.goauthentik.io/* { reverse_proxy authentik-server:9000 } # sans effet
reverse_proxy /outpost.goauthentik.io/* authentik-server:9000 # sans effet
forward_auth authentik-server:9000 { ... }
}
Le seul endroit où elle prend est le site, avec l'application elle aussi dans un
handle : deux handle frères sont des routes mutuellement exclusives, et la plus
spécifique gagne avant que forward_auth n'entre en jeu.
site.savethemall.eu {
import tls_cert site.savethemall.eu
handle /outpost.goauthentik.io/* {
reverse_proxy authentik-server:9000
}
handle {
import authentik
reverse_proxy monservice:8080
}
}
La sonde qui distingue les deux formes en une requête, sans compte ni session :
curl -s -o /dev/null -w '%{http_code}\n' https://<site>/outpost.goauthentik.io/zzz
# 404 → la route outpost existe, c'est Authentik qui répond
# 302 → pas de route ; forward_auth a intercepté
Ce n'est pas une panne pour les sites qui ne l'ont pas
Sur 39 sites qui importent authentik, 4 portent ce handle. Les 35 autres
fonctionnent. Le handle est un durcissement conforme à la documentation
Authentik, pas une condition de fonctionnement. Généraliser la forme demande
de réécrire 35 blocs un par un — un chantier à froid, avec la sonde comme
critère, pas un sed.
OIDC d'abord, forward_auth ensuite, jamais les deux¶
Pour un nouveau service, chercher d'abord
integrations.goauthentik.io/documentation/<service>. Si l'application sait
faire OIDC, la brancher dessus.
forward_auth ne connaît pas l'identité applicative : ni compte, ni rôle, ni
déconnexion propre — juste un mur. Il reste le bon choix pour ce qui n'a pas de
notion d'utilisateur, un site statique par exemple.
Les deux ensemble sont un piège
Le proxy intercepte /api/auth/callback/* avant l'application → boucle de
connexion, avec trois composants qui ont tous l'air sains.
Fermer la connexion locale (DISABLE_PASSWORD_AUTH) après la première
connexion OIDC réussie, jamais avant.
Certificats¶
- Émis par acme.sh en DNS-01 chez OVH, un certificat par domaine, dans
/opt/docker/acme/certs/<fqdn>/. - Chargés explicitement :
auto_https offdans les deux Caddy, et une macro(tls_cert)par site. - Rechargement après renouvellement par l'API d'administration de Caddy
(
POST http://127.0.0.1:2019/load), publiée sur la boucle locale uniquement. - Le NAS n'émet rien : les certificats lui sont copiés par un
rsyncquotidien depuis vm-docker.
Détail qui mord : un upstream désigné par son IP¶
Caddy envoie alors Host: nas:443. La plupart des applications rendent
des redirections absolues à partir de cet en-tête, et le client part vers une
adresse interne. Toujours ajouter :
Dernière vérification de cette page : 2026-09-08