Reverse-proxy recipes and the callback contract
Every reverse proxy needs the same two pieces: an authorizer check that asks Caputchin whether a request carries a valid pass, and a callback on your origin that turns a solved pass into the cpt_gate cookie. The nginx walkthrough shows the shape end to end; this page gives the other proxies, the exact callback contract, and a full Authelia example. Replace YOUR_SITE_KEY with your public key throughout.
The endpoints are the same everywhere:
- Authorizer —
GET https://verify.caputchin.com/v1/gate/authz?site=YOUR_SITE_KEY, with the visitor'sCookieheader forwarded.204means allow,401means challenge. - Challenge — redirect the browser to
https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return=<the original URL>.
Traefik
Use a forwardAuth middleware for the authorizer. Traefik forwards the incoming Cookie header by default. On a 401 from the middleware, redirect the browser to the challenge (an error-page middleware pointed at a small route that issues the 302, or handle the 401 at your 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-appCaddy
Use forward_auth for the authorizer and handle_errors to turn the 401 into a redirect. This is a starting point; adjust directive syntax for your Caddy version.
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
}The callback
The hosted challenge page finishes by auto-submitting a POST form to https://<your-origin>/__cpt/callback with two fields in the body: cpt_gate (the pass) and to (where to send the visitor). Add this one route to your origin (or as a dedicated location on the proxy). It must:
- Read
cpt_gateandtofrom the POST body, never the URL (a pass in a URL leaks into logs and referrers). - Confirm
tois on your own origin, and reject anything else, so the callback cannot be turned into an open redirect. - Set the cookie with the attributes below.
- Redirect (
303) toto.
// 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);
});The cookie attributes are the whole security model. Because the pass is not tied to a device, HttpOnly (blocks script theft), Secure (HTTPS only), SameSite=Lax, and a short Max-Age (match your Clearance TTL) are what protect it. Also: do not log the cpt_gate value in your access logs, and keep Referrer-Policy: no-referrer on the callback so the pass never rides along in a referrer header.
Worked example: Authelia
Authelia's login portal is a compiled single-page app with no place to embed the widget and no plugin hook, so the gate is the way to protect it. Authelia already sits behind a reverse proxy (that is how its own ForwardAuth works), and that proxy is where the gate goes. Authelia itself is left completely unmodified.
Put the gate on the portal's vhost (for example auth.example.com):
- Gate the portal HTML and the first-factor login endpoint (
/api/firstfactor). - Exclude Authelia's own ForwardAuth verify endpoint (
/api/verify), which the proxy calls for every protected upstream request. Gating it would break every protected app behind Authelia. - Exclude health checks and the static assets the challenge page itself needs.
- Add the
/__cpt/callbackroute as above.
The gate keeps bots off the portal; Authelia's built-in Regulation (retry limit and ban) still caps credential retries for whoever passes, so the two layers stack. The gate protects the interactive portal (the login and consent UI) only; it does not, and should not, gate non-interactive token endpoints.
See also
- Set up the Proxy page-gate — the nginx walkthrough and preview-mode-first rollout.
- Overview — the concept and the clearance cookie.
- Statistics — confirm the wiring by watching challenge-vs-passthrough.