---
type: how-to
title: Reverse-proxy recipes and the callback contract
summary: Wire the Proxy page-gate on Traefik or Caddy, host the callback that turns a solved pass into the cookie, and gate Authelia without touching it.
---

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](/docs/proxy-page-gate/set-up) 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's `Cookie` header forwarded. `204` means allow, `401` means 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).

```yaml
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

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.

```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
}
```

## 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:

1. Read `cpt_gate` and `to` from the POST **body**, never the URL (a pass in a URL leaks into logs and referrers).
2. Confirm `to` is on your own origin, and reject anything else, so the callback cannot be turned into an open redirect.
3. Set the cookie with the attributes below.
4. Redirect (`303`) to `to`.

```js
// 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/callback` route 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](/docs/proxy-page-gate/set-up) — the nginx walkthrough and preview-mode-first rollout.
- [Overview](/docs/proxy-page-gate/overview) — the concept and the clearance cookie.
- [Statistics](/docs/proxy-page-gate/statistics) — confirm the wiring by watching challenge-vs-passthrough.
