Aller au contenu

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, et sudo y 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 off dans 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 rsync quotidien depuis vm-docker.

Détail qui mord : un upstream désigné par son IP

reverse_proxy https://le NAS

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 :

reverse_proxy https://le NAS {
    header_up Host {host}
}

Dernière vérification de cette page : 2026-09-08