Caputchin
Puerta de página en el 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>.

El autorizador responde solo 204 o 401. Cualquier otra cosa, incluido un timeout, significa que Caputchin es inalcanzable, y lo que pase entonces es tu modo de fallo: bloquear la petición (fallo cerrado, más seguro para un portal de inicio de sesión) o dejarla pasar (fallo abierto). Ponlo en la clave de sitio y el fragmento generado vendrá igual. Dale a la subpetición del autorizador un timeout corto, para que una caída no pueda atascar todas las peticiones mientras decide.

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 verificación 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). Si cambiaste la ruta del callback en la clave de sitio, el POST va ahí en su lugar. 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.

Modo de fallo por proxy

Caputchin nunca ve una decisión de modo de fallo, así que nunca aparece en tus estadísticas: tu proxy la toma localmente cuando no puede alcanzar al autorizador.

  • nginx: proxy_intercept_errors on; más error_page 500 502 503 504 = @cpt_unreachable;, donde ese location con nombre hace return 403 (cerrado) o return 204 (abierto).
  • Traefik: el middleware forwardAuth da error ante una dirección inalcanzable; enruta eso a un servicio que devuelva 403 (cerrado), o quita el middleware del router detrás de un healthcheck (abierto).
  • Caddy: amplía handle_errors con un matcher sobre 5xx que haga respond 403 (cerrado) o respond 204 (abierto).

Alcance por rutas, proxy a proxy

Cubrir solo estas rutas y No cubrir nunca estas rutas dan forma a la configuración generada; tu proxy es quien las hace cumplir.

  • nginx: un location ^~ <prefijo> por cada ruta con puerta, y auth_request off; dentro de cada una excluida. Un patrón de extensión como *.php se vuelve location ~* \.php$.
  • Traefik: engancha el middleware cpt-gate solo a los routers cuyas reglas casen con las rutas con puerta, y déjalo fuera del resto.
  • Caddy: envuelve forward_auth en un matcher de ruta, por ejemplo @gated path /admin/* /login y luego forward_auth @gated ....

Nunca pongas puerta a la subpetición del autorizador ni a tu ruta de callback.

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 (o tu ruta de callback configurada).

Esos cuatro puntos son exactamente lo que expresan los ajustes de alcance de rutas: / y /api/firstfactor como rutas con puerta, /api/verify y tu healthcheck como rutas excluidas.

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