Caputchin
プロキシゲート

リバースプロキシのレシピとコールバック契約

Markdown で表示

どのリバースプロキシにも同じ二つの部品が要ります。リクエストが有効なパスを持つか Caputchin に尋ねる オーソライザー チェックと、解かれたパスを cpt_gate Cookie に変える、あなたのオリジン上の コールバック です。nginx の手順 が端から端までの形を示します。このページは他のプロキシ、正確なコールバック契約、そして 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> へリダイレクトします。

Traefik

オーソライザーには forwardAuth ミドルウェアを使います。Traefik は入ってくる Cookie ヘッダーを既定で転送します。ミドルウェアからの 401 では、ブラウザをチャレンジへリダイレクトします(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 を、401 をリダイレクトに変えるには handle_errors を使います。これは出発点です。ディレクティブの構文はあなたの 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
}

コールバック

ホスト型のチャレンジページは終わりに、https://<あなたのオリジン>/__cpt/callbackPOST フォームを自動送信します。ボディ には二つのフィールド、cpt_gate(パス)と to(訪問者をどこへ送るか)が入ります。この一つのルートをあなたのオリジンに足してください(またはプロキシ上の専用ロケーションとして)。それは次を満たす必要があります:

  1. cpt_gateto を POST の ボディ から読み、URL からは決して読まない(URL の中のパスはログやリファラーに漏れます)。
  2. to があなた自身のオリジン上だと確認し、それ以外はすべて拒否する。そうすればコールバックをオープンリダイレクトに変えられません。
  3. 下の属性で Cookie をセットする。
  4. to へ(303 で)リダイレクトする。
// 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 の値をアクセスログに記録しないでください。そしてコールバックに Referrer-Policy: no-referrer を保ち、パスがリファラーヘッダーに乗って一緒に運ばれないようにしてください。

実例:Authelia

Authelia のログインポータルは、ウィジェットを埋め込む場所もプラグインフックもない、コンパイル済みのシングルページアプリなので、ゲートがそれを守るやり方です。Authelia はすでにリバースプロキシの背後に座っており(自身の ForwardAuth はそう動きます)、そのプロキシがゲートの置き場所です。Authelia 自体はまったく変更されずに残されます。

ゲートをポータルの vhost(たとえば auth.example.com)に置きます:

  • ポータルの HTML と第一要素のログインエンドポイント(/api/firstfactor)に ゲートをかける
  • Authelia 自身の ForwardAuth の verify エンドポイント(/api/verify)を 除外する。プロキシは保護された各アップストリームのリクエストごとにこれを呼びます。これにゲートをかけると、Authelia の背後の保護された各アプリが壊れます。
  • ヘルスチェックと、チャレンジページ自体が必要とする静的アセットを 除外する
  • 上のとおり /__cpt/callback ルートを 足す

ゲートはボットをポータルから遠ざけます。Authelia 組み込みの Regulation(再試行の上限と禁止)は、通った者の資格情報の再試行をなお抑えるので、二つの層は重なります。ゲートはインタラクティブなポータル(ログインと同意の UI)だけを守ります。非インタラクティブなトークンのエンドポイントにはゲートをかけませんし、かけるべきでもありません。

あわせて読む

このページの内容