Caputchin
Porte de proxy

Recettes de proxy inverse et le contrat du callback

Voir en Markdown

Chaque proxy inverse a besoin des deux mêmes pièces : un contrôle autorisateur qui demande à Caputchin si une requête porte un laissez-passer valide, et un callback sur ton origine qui transforme un laissez-passer résolu en cookie cpt_gate. La marche à suivre nginx montre la forme de bout en bout ; cette page donne les autres proxies, le contrat exact du callback, et un exemple Authelia complet. Remplace YOUR_SITE_KEY par ta clé publique partout.

Les endpoints sont les mêmes partout :

  • Autorisateur : GET https://verify.caputchin.com/v1/gate/authz?site=YOUR_SITE_KEY, avec l'en-tête Cookie du visiteur transmis. 204 veut dire autoriser, 401 veut dire défi.
  • Défi : redirige le navigateur vers https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return=<l'URL originale>.

Traefik

Utilise un middleware forwardAuth pour l'autorisateur. Traefik transmet l'en-tête Cookie entrant par défaut. Sur un 401 du middleware, redirige le navigateur vers le défi (un middleware de page d'erreur pointé vers une petite route qui émet le 302, ou gère le 401 à ton edge).

http:
  middlewares:
    cpt-gate:
      forwardAuth:
        address: "https://verify.caputchin.com/v1/gate/authz?site=YOUR_SITE_KEY"
        # The 401 from forwardAuth is your signal to send the browser to:
        #   https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return=<original-url>
  routers:
    portal:
      rule: "Host(`auth.example.com`)"
      middlewares: ["cpt-gate"]
      service: your-app

Caddy

Utilise forward_auth pour l'autorisateur et handle_errors pour transformer le 401 en redirection. C'est un point de départ ; ajuste la syntaxe des directives pour ta version de Caddy.

auth.example.com {
  forward_auth https://verify.caputchin.com {
    uri /v1/gate/authz?site=YOUR_SITE_KEY
    copy_headers Cookie
  }
  handle_errors {
    @challenge expression {http.error.status_code} == 401
    redir @challenge https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return={scheme}://{host}{uri} 302
  }
  reverse_proxy your-app:8080
}

Le callback

La page de défi hébergée se termine en auto-envoyant un formulaire POST vers https://<ton-origine>/__cpt/callback avec deux champs dans le corps : cpt_gate (le laissez-passer) et to (où envoyer le visiteur). Ajoute cette seule route à ton origine (ou comme une location dédiée sur le proxy). Elle doit :

  1. Lire cpt_gate et to depuis le corps du POST, jamais l'URL (un laissez-passer dans une URL fuit dans les logs et les référents).
  2. Confirmer que to est sur ta propre origine, et rejeter tout le reste, pour que le callback ne puisse pas être transformé en open redirect.
  3. Poser le cookie avec les attributs ci-dessous.
  4. Rediriger (303) vers to.
// Any tiny handler on your origin works. Express shown; ~15 lines.
app.post("/__cpt/callback", express.urlencoded({ extended: false }), (req, res) => {
  const pass = String(req.body.cpt_gate || "");
  const to = String(req.body.to || "/");
  const target = new URL(to, `https://${req.headers.host}`);
  if (target.host !== req.headers.host) return res.status(400).end(); // same-origin only
  res.setHeader(
    "Set-Cookie",
    `cpt_gate=${pass}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=1800`
  );
  res.redirect(303, target.pathname + target.search);
});

Les attributs du cookie sont tout le modèle de sécurité. Comme le laissez-passer n'est pas lié à un appareil, HttpOnly (bloque le vol par script), Secure (HTTPS seulement), SameSite=Lax, et un Max-Age court (à faire correspondre à ton TTL de laissez-passer) sont ce qui le protège. Aussi : ne journalise pas la valeur cpt_gate dans tes access logs, et garde Referrer-Policy: no-referrer sur le callback pour que le laissez-passer ne voyage jamais dans un en-tête référent.

Exemple travaillé : Authelia

Le portail de connexion d'Authelia est une app mono-page compilée sans place pour incruster le widget et sans hook de plugin, donc la porte est le moyen de le protéger. Authelia est déjà derrière un proxy inverse (c'est ainsi que son propre ForwardAuth fonctionne), et ce proxy est là où va la porte. Authelia lui-même reste complètement inchangé.

Mets la porte sur le vhost du portail (par exemple auth.example.com) :

  • Mets une porte au HTML du portail et à l'endpoint de connexion de premier facteur (/api/firstfactor).
  • Exclus l'endpoint verify du propre ForwardAuth d'Authelia (/api/verify), que le proxy appelle pour chaque requête upstream protégée. Y mettre une porte casserait chaque app protégée derrière Authelia.
  • Exclus les health checks et les assets statiques dont la page du défi elle-même a besoin.
  • Ajoute la route /__cpt/callback comme ci-dessus.

La porte tient les bots hors du portail ; la Regulation intégrée d'Authelia (limite de réessais et bannissement) plafonne toujours les réessais d'identifiants pour quiconque passe, donc les deux couches se cumulent. La porte protège seulement le portail interactif (l'UI de connexion et de consentement) ; elle ne met pas de porte, et ne devrait pas, aux endpoints de token non interactifs.

Voir aussi

Sur cette page