Vai al contenuto

Proxy inverso — nota di architettura

Traduzione automatica dell'originale francese.

Il percorso completo

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

Perché due livelli

Uno sarebbe stato sufficiente dal punto di vista funzionale. Ne esistono due perché le due macchine non sono in grado di fare la stessa cosa:

  • Il NAS è l'unico che vede la rete WAN. Il NAT del router punta verso di esso. Pertanto, ospita i siti statici che devono sopravvivere alla morte di vm-docker.
  • vm-docker è l'unico che può bloccare. Il NAS gira su un kernel DSM senza xt_set, senza albero /lib/modules, e sudo richiede una password: non è possibile alcun blocco L3. La soluzione CrowdSec risiede su vm-docker, con un blocco applicativo di tipo 403.

Le due conseguenze che comportano ore di lavoro

Un filtro L3 posizionato su vm-docker non blocca nulla

Tutte le richieste WAN arrivano su vm-docker con nas come sorgente. Una regola per IP client è strutturalmente cieca.

Ciò che funziona: trusted_proxies static le NAS/32 nelle opzioni globali di Caddy, che gli permette di leggere il X-Forwarded-For di nas-caddy. I moduli che consumano l'IP client (incluso CrowdSec) vedono quindi la vera identità.

I siti statici serviti dal NAS non passano attraverso CrowdSec

Non attraversano mai il Caddy di vm-docker. La soluzione L7 non li copre. È una limitazione intenzionale, non un errore.

L'autenticazione

Il frammento condiviso, nel Caddyfile di 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
    }
}

E il suo equivalente nel vero edge, per i siti statici serviti localmente dal 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
    }
}

Ciò che forward_auth fa, richiesta per richiesta

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

Il servizio non vede mai la password: riceve una richiesta già autorizzata, con l'identità negli header. È anche il suo limite: non ha modo di sapere se una sessione è terminata.

forward_auth avviene prima di 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

Suoi 39 siti che utilizzano authentik, 4 presentano questa handle. Gli altri 35 funzionano. Il handle è un rafforzamento conforme alla documentazione Authentik, non una condizione di funzionamento. Generalizzare la forma richiederebbe di riscrivere 35 blocchi uno alla volta — un'operazione complessa, con la sonda come criterio, non un sed.

OIDC prima, forward_auth poi, mai entrambi

Per un nuovo servizio, cercare prima integrations.goauthentik.io/documentation/<service>. Se l'applicazione è in grado di utilizzare OIDC, collegarla.

forward_auth non conosce l'identità applicativa: né account, né ruolo, né disconnessione — solo un muro. Rimane la scelta giusta per chi non ha una conoscenza dell'utente, ad esempio un sito statico.

Entrambi insieme sono una trappola

Il proxy intercetta /api/auth/callback/* prima che l'applicazione lo faccia → creando un ciclo di connessione, con tre componenti che sembrano funzionare correttamente.

Chiudere la connessione locale (DISABLE_PASSWORD_AUTH) dopo la prima connessione OIDC riuscita, mai prima.

Certificati

  • Emessi da acme.sh tramite DNS-01 presso OVH, un certificato per dominio, in /opt/docker/acme/certs/<fqdn>/.
  • Caricati esplicitamente: auto_https off in entrambi i Caddy, e una macro (tls_cert) per sito.
  • Ricaricati dopo il rinnovo tramite l'API di amministrazione di Caddy (POST http://127.0.0.1:2019/load), pubblicata solo sulla connessione locale.
  • Il NAS non emette nulla: i certificati gli vengono copiati quotidianamente da rsync da vm-docker.

Dettaglio che fa la differenza: un server upstream specificato tramite IP

reverse_proxy https://le NAS

Caddy invia quindi Host: nas:443. La maggior parte delle applicazioni effettua reindirizzamenti assoluti a partire da questo header, e il client si connette a un indirizzo interno. È sempre necessario aggiungere:

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

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