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> へリダイレクトします。

オーソライザーが返すのは 204 か 401 だけ です。タイムアウトを含めそれ以外は Caputchin に到達できない意味で、そのとき何が起きるかは、あなたの フェイルモード です。リクエストをブロックする(fail closed、ログインポータルには安全寄り)か、通す(fail open)か。サイトキーで設定すれば、生成されるスニペットもそれに合います。オーソライザーのサブリクエストには短いタイムアウトを与え、障害が判断の間に全リクエストを止めてしまわないようにしてください。

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/callback へ POST フォームを自動送信します。ボディ には二つのフィールド、cpt_gate(パス)と to(訪問者をどこへ送るか)が入ります。サイトキーで コールバックパス を変えたなら、代わりにそちらへ送信します。この一つのルートをオリジンに足してください(またはプロキシ上の専用ロケーションとして)。それは次を満たす必要があります:

  1. cpt_gate と to を 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 を保ち、パスがリファラーヘッダーに乗って一緒に運ばれないようにしてください。

プロキシごとのフェイルモード

Caputchin はフェイルモードの判断を決して見ないので、統計にも決して現れません。オーソライザーに到達できないとき、プロキシがローカルで下すからです。

  • nginx:proxy_intercept_errors on; に加えて error_page 500 502 503 504 = @cpt_unreachable;。その名前付きロケーションを return 403(クローズド)か return 204(オープン)にします。
  • Traefik:forwardAuth ミドルウェアは到達できないアドレスでエラーになります。それを 403 を返すサービスへルーティングする(クローズド)か、ヘルスチェックの後ろでルーターからミドルウェアを外す(オープン)。
  • Caddy:handle_errors を 5xx のマッチャーで広げ、respond 403(クローズド)か respond 204(オープン)にします。

プロキシごとのパスの範囲

これらのパスだけをゲートする と これらのパスは決してゲートしない は、生成される設定を形づくります。強制するのはあなたのプロキシです。

  • nginx:ゲートをかけるパスごとに location ^~ <接頭辞> を一つ、除外する各パスの中には auth_request off; を置きます。*.php のような拡張子のパターンは location ~* \.php$ になります。
  • Traefik:cpt-gate ミドルウェアは、ルールがゲート対象のパスに合うルーターにだけ付け、残りには付けません。
  • Caddy:forward_auth をパスのマッチャーで包みます。たとえば @gated path /admin/* /login としてから forward_auth @gated ...。

オーソライザーのサブリクエストと、あなたのコールバックのパスには決してゲートをかけないでください。

実例:Authelia

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

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

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

この四つの項目こそ、パスの範囲 の設定が表すものです。/ と /api/firstfactor をゲート対象のパスに、/api/verify とヘルスチェックを除外するパスにします。

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

あわせて読む

このページの内容