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>.
The authorizer answers only 204 or 401. Anything else, including a timeout, means Caputchin is unreachable, and what happens then is your fail mode: block the request (fail closed, safer for a login portal) or let it through (fail open). Set it on the site key and the generated snippet matches. Give the authorizer subrequest a short timeout so an outage cannot stall every request while it decides.
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 check 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). If you changed the callback path on the site key, it posts there instead. 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.
Fail mode per proxy
Caputchin never sees a fail-mode decision, so it never appears in your statistics: your proxy makes it locally when it cannot reach the authorizer.
- nginx —
proxy_intercept_errors on;pluserror_page 500 502 503 504 = @cpt_unreachable;, where that named location isreturn 403(closed) orreturn 204(open). - Traefik — the
forwardAuthmiddleware errors out on an unreachable address; route that to a service returning403(closed), or drop the middleware from the router behind a healthcheck (open). - Caddy — extend
handle_errorswith a matcher on5xxthat eitherrespond 403(closed) orrespond 204(open).
Path scoping per proxy
Gate only these paths and Never gate these paths shape the generated configuration; your proxy enforces them.
- nginx — one
location ^~ <prefix>per gated path, andauth_request off;inside each excluded one. An extension pattern like*.phpbecomeslocation ~* \.php$. - Traefik — attach the
cpt-gatemiddleware only to the routers whose rules match the gated paths, and leave it off the rest. - Caddy — wrap
forward_authin a path matcher, for example@gated path /admin/* /loginthenforward_auth @gated ....
Never gate the authorizer subrequest or your callback path.
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 (or your configured callback path).
Those four bullets are exactly what the path scope settings express: / and /api/firstfactor as gated paths, /api/verify and your healthcheck as excluded ones.
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.
- Customize the challenge — challenge type, language, theme, cookie name, callback path, path scope.
- Overview — the concept and the clearance cookie.
- Statistics — confirm the wiring by watching challenge-vs-passthrough.