Caputchin
Страничный шлюз на прокси

Рецепты обратного прокси и контракт callback

Открыть как Markdown

Каждому обратному прокси нужны те же две части: проверка авторизатора, которая спрашивает Caputchin, несёт ли запрос валидный пропуск, и callback на твоём источнике, который превращает решённый пропуск в cookie cpt_gate. Прохождение с nginx показывает форму от края до края; эта страница даёт другие прокси, точный контракт callback, и полный пример Authelia. Замени YOUR_SITE_KEY своим публичным ключом повсюду.

Эндпоинты везде одни и те же:

  • Авторизатор: GET https://verify.caputchin.com/v1/gate/authz?site=YOUR_SITE_KEY, с пересланным заголовком Cookie посетителя. 204 значит разрешить, 401 значит испытание.
  • Испытание: перенаправь браузер на https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return=<исходный URL>.

Авторизатор отвечает только 204 или 401. Всё остальное, включая таймаут, значит, что Caputchin недостижим, и что будет дальше, решает твой режим отказа: заблокировать запрос (fail closed, безопаснее для портала входа) или пропустить его (fail open). Задай его на ключе сайта, и сгенерированный сниппет совпадёт. Дай подзапросу авторизатора короткий таймаут, чтобы сбой не смог затормозить каждый запрос, пока он решает.

Traefik

Используй middleware forwardAuth для авторизатора. Traefik пересылает входящий заголовок Cookie по умолчанию. При 401 от middleware перенаправь браузер на испытание (middleware страницы ошибки, указывающий на маленький маршрут, который выдаёт 302, или обработай 401 на своём краю).

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

Используй forward_auth для авторизатора и handle_errors, чтобы превратить 401 в редирект. Это отправная точка; подстрой синтаксис директив под свою версию 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
}

Callback

Размещённая страница проверки завершается автоотправкой формы POST на https://<твой-источник>/__cpt/callback с двумя полями в теле: cpt_gate (пропуском) и to (куда отправить посетителя). Если ты сменил путь callback на ключе сайта, POST уходит туда. Добавь этот один маршрут на свой источник (или как отдельный location на прокси). Он должен:

  1. Читать cpt_gate и to из тела POST, никогда из URL (пропуск в URL утекает в логи и рефереры).
  2. Подтвердить, что to на твоём собственном источнике, и отклонить всё остальное, чтобы callback нельзя было превратить в open redirect.
  3. Поставить cookie с атрибутами ниже.
  4. Перенаправить (303) на 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);
});

Атрибуты cookie составляют всю модель безопасности. Поскольку пропуск не привязан к устройству, HttpOnly (блокирует кражу скриптом), Secure (только HTTPS), SameSite=Lax, и короткий Max-Age (в тон твоему TTL пропуска) это то, что его защищает. Ещё: не логируй значение cpt_gate в своих access-логах, и держи Referrer-Policy: no-referrer на callback, чтобы пропуск никогда не ехал попутчиком в заголовке реферера.

Режим отказа для каждого прокси

Caputchin никогда не видит решения о режиме отказа, так что оно никогда не появляется в твоей статистике: твой прокси принимает его локально, когда не может достучаться до авторизатора.

  • nginx: proxy_intercept_errors on; плюс error_page 500 502 503 504 = @cpt_unreachable;, где этот именованный location делает return 403 (закрытый) или return 204 (открытый).
  • Traefik: middleware forwardAuth падает с ошибкой на недостижимом адресе; направь это на сервис, отдающий 403 (закрытый), или убери middleware с роутера за healthcheck (открытый).
  • Caddy: расширь handle_errors матчером на 5xx, который делает либо respond 403 (закрытый), либо respond 204 (открытый).

Ограничение по путям для каждого прокси

Закрывать шлюзом только эти пути и Никогда не закрывать шлюзом эти пути формируют сгенерированную конфигурацию; принуждает к ним твой прокси.

  • nginx: один location ^~ <префикс> на каждый закрытый путь и auth_request off; внутри каждого исключённого. Шаблон по расширению вроде *.php становится location ~* \.php$.
  • Traefik: вешай middleware cpt-gate только на те роутеры, чьи правила совпадают с закрытыми путями, и не ставь его на остальные.
  • Caddy: заверни forward_auth в матчер пути, например @gated path /admin/* /login, а затем forward_auth @gated ....

Никогда не закрывай подзапрос авторизатора и свой путь callback.

Разобранный пример: Authelia

Портал входа Authelia представляет собой скомпилированное одностраничное приложение без места, чтобы встроить виджет, и без хука плагина, так что шлюз и есть способ его защитить. Authelia уже сидит за обратным прокси (так работает её собственный ForwardAuth), и этот прокси и есть место для шлюза. Саму Authelia оставляют полностью без изменений.

Поставь шлюз на vhost портала (например auth.example.com):

  • Закрой HTML портала и эндпоинт входа первого фактора (/api/firstfactor).
  • Исключи собственный verify-эндпоинт ForwardAuth Authelia (/api/verify), который прокси вызывает на каждый защищённый upstream-запрос. Закрыть его сломало бы каждое защищённое приложение за Authelia.
  • Исключи health-check и статические ассеты, которые нужны самой странице испытания.
  • Добавь маршрут /__cpt/callback как выше (или свой настроенный путь callback).

Эти четыре пункта и есть ровно то, что выражают настройки области путей: / и /api/firstfactor как закрытые пути, /api/verify и твой healthcheck как исключённые.

Шлюз держит ботов подальше от портала; встроенная Regulation Authelia (лимит повторов и бан) по-прежнему ограничивает повторы учётных данных для того, кто прошёл, так что два слоя складываются. Шлюз защищает только интерактивный портал (UI входа и согласия); он не закрывает, и не должен, неинтерактивные эндпоинты токенов.

См. также

На этой странице