Caputchin
Puerta de proxy

Recetas de proxy inverso y el contrato del callback

Ver como Markdown

Todo proxy inverso necesita las mismas dos piezas: una comprobación autorizador que pregunta a Caputchin si una petición lleva un pase válido, y un callback en tu origen que convierte un pase resuelto en la cookie cpt_gate. El recorrido con nginx muestra la forma de extremo a extremo; esta página da los otros proxies, el contrato exacto del callback, y un ejemplo completo de Authelia. Reemplaza YOUR_SITE_KEY con tu clave pública en todo momento.

Los endpoints son los mismos en todas partes:

  • Autorizador: GET https://verify.caputchin.com/v1/gate/authz?site=YOUR_SITE_KEY, con la cabecera Cookie del visitante reenviada. 204 significa permitir, 401 significa reto.
  • Reto: redirige el navegador a https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return=<la URL original>.

Traefik

Usa un middleware forwardAuth para el autorizador. Traefik reenvía la cabecera Cookie entrante por defecto. Ante un 401 del middleware, redirige el navegador al reto (un middleware de página de error apuntado a una pequeña ruta que emite el 302, o maneja el 401 en tu borde).

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

Usa forward_auth para el autorizador y handle_errors para convertir el 401 en una redirección. Esto es un punto de partida; ajusta la sintaxis de las directivas para tu versión 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
}

El callback

La página de reto alojada termina auto-enviando un formulario POST a https://<tu-origen>/__cpt/callback con dos campos en el cuerpo: cpt_gate (el pase) y to (a dónde enviar al visitante). Añade esta única ruta a tu origen (o como un location dedicado en el proxy). Debe:

  1. Leer cpt_gate y to desde el cuerpo del POST, nunca la URL (un pase en una URL se filtra a logs y referrers).
  2. Confirmar que to está en tu propio origen, y rechazar cualquier otra cosa, para que el callback no pueda convertirse en un open redirect.
  3. Fijar la cookie con los atributos de abajo.
  4. Redirigir (303) a 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);
});

Los atributos de la cookie son todo el modelo de seguridad. Como el pase no está atado a un dispositivo, HttpOnly (bloquea el robo por script), Secure (solo HTTPS), SameSite=Lax, y un Max-Age corto (que empareje tu TTL de paso libre) son lo que lo protege. Además: no registres el valor cpt_gate en tus access logs, y mantén Referrer-Policy: no-referrer en el callback para que el pase nunca viaje de acompañante en una cabecera referrer.

Ejemplo resuelto: Authelia

El portal de inicio de sesión de Authelia es una app de una sola página compilada, sin lugar donde incrustar el widget y sin hook de plugin, así que la puerta es la manera de protegerlo. Authelia ya vive tras un proxy inverso (así funciona su propio ForwardAuth), y ese proxy es donde va la puerta. Authelia en sí se deja completamente sin modificar.

Pon la puerta en el vhost del portal (por ejemplo auth.example.com):

  • Pon puerta al HTML del portal y al endpoint de inicio de sesión de primer factor (/api/firstfactor).
  • Excluye el endpoint verify del propio ForwardAuth de Authelia (/api/verify), que el proxy llama en cada petición upstream protegida. Ponerle puerta rompería cada app protegida tras Authelia.
  • Excluye los health checks y los assets estáticos que la propia página del reto necesita.
  • Añade la ruta /__cpt/callback como arriba.

La puerta mantiene los bots fuera del portal; la Regulation incorporada de Authelia (límite de reintentos y baneo) sigue topando los reintentos de credenciales de quien pase, así que las dos capas se apilan. La puerta protege solo el portal interactivo (la UI de inicio de sesión y consentimiento); no pone puerta, ni debe, a los endpoints de token no interactivos.

Véase también

En esta página