Caputchin
Proxy page-gate

Set up the Proxy page-gate with nginx

View as Markdown

By the end of this tutorial the gate will be running in front of a site behind nginx: an un-cleared visitor is sent to a check, passing it sets a cookie, and later requests pass straight through. You will watch the whole thing in preview mode first, so nothing is blocked until you have confirmed the wiring. nginx is used here; Traefik, Caddy, and Authelia recipes follow the same shape.

1. Turn on Preview mode

In the dashboard, open the troop and turn on Preview mode. While it is on, the gate authorizer records challenge-vs-passthrough decisions to the statistics page but never blocks. You will turn it off at the end.

2. Enable the gate on your site key

Open your site key and go to the Proxy gate page. Turn on Status → Enabled, then set:

  • Challenge type — Play a game (the default) or Confirm with a checkbox. A site key that requires a game always shows a game, and the page says so. Choosing a game needs at least one installed game with a replayable artifact.
  • Clearance TTL — how long one solve clears a visitor (default 30 minutes). Shorter means more re-checks but a smaller window if a cookie is stolen.
  • Fail mode — what your proxy does when it cannot reach Caputchin's authorizer. Fail closed blocks (safer for a login portal); fail open lets requests through (avoids downtime on a public site).

Leave Language and Theme on match the visitor: the check then speaks the visitor's language, follows their light or dark preference, and carries this site key's branding automatically. Under Advanced you can rename the gate cookie, move the callback path, and scope the gate to specific paths; the defaults are what this walkthrough uses. See Customize the challenge.

The page also shows a ready-to-copy nginx snippet with your settings already filled in. Settings take up to ten seconds to reach the edge after you change them.

3. Allow the origin you are gating

The check only ever redirects back to an allowed origin, the same origin allowlist the widget uses. Make sure it includes the origin you are gating, for example https://auth.example.com. Set it on the site key's Cap configuration if it is not there already.

4. Wire the authorizer and the challenge redirect

Two location blocks: an internal authorizer subrequest, and a challenge redirect the proxy jumps to on a 401. Replace YOUR_SITE_KEY with your public key.

# Gate the protected paths.
location / {
  auth_request /_cpt_authz;             # 204 = allow, 401 = challenge
  error_page 401 = @cpt_challenge;
  proxy_intercept_errors on;
  error_page 500 502 503 504 = @cpt_unreachable;
  # ...proxy_pass to your app...
}

location = /_cpt_authz {
  internal;
  auth_request off;
  proxy_pass https://verify.caputchin.com/v1/gate/authz?site=YOUR_SITE_KEY;
  proxy_pass_request_body off;
  proxy_set_header Content-Length "";
  proxy_set_header Cookie $http_cookie;  # forward the cpt_gate cookie
  proxy_connect_timeout 2s;
  proxy_read_timeout 2s;
}

location @cpt_challenge {
  return 302 https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return=$scheme://$host$request_uri;
}

# This is your fail mode. `return 403` blocks when Caputchin is unreachable
# (fail closed); `return 204` lets the request through (fail open).
location @cpt_unreachable {
  return 403;
}

The authorizer only ever answers 204 or 401, so anything else, including a timeout, means Caputchin is unreachable and your fail mode decides. Copy the snippet from the dashboard and it already matches the fail mode you picked.

Gate the entry paths (the portal HTML and the login POST). Do not gate the authorizer subrequest, health checks, or the callback below.

The check page finishes by auto-submitting a small POST to /__cpt/callback on your origin (or whatever callback path you set), with cpt_gate (the pass) and to (where to send the visitor) in the body. The field is named cpt_gate even if you renamed the cookie. A tiny handler sets the cookie and redirects. The full contract, including why each cookie attribute matters, is in reverse-proxy recipes. The minimum:

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);
});

6. Watch it in preview

Load the gated URL. Because preview is on, you pass straight through, but the statistics page records a challenge_served (you had no cookie) and, once you solve and the callback runs, a pass_issued. Reload and you should see passthrough climb. If challenges never appear, the authorizer block is not being hit; if the callback 400s, check the to origin.

7. Go live

When the numbers look right, turn Preview mode off. The gate now enforces: un-cleared visitors are sent to the game, solved visitors roam until their pass expires.

Where to go next

On this page