リバースプロキシのレシピとコールバック契約
どのリバースプロキシにも同じ二つの部品が要ります。リクエストが有効なパスを持つか 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-appCaddy
オーソライザーには 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(訪問者をどこへ送るか)が入ります。この一つのルートをあなたのオリジンに足してください(またはプロキシ上の専用ロケーションとして)。それは次を満たす必要があります:
cpt_gateとtoを POST の ボディ から読み、URL からは決して読まない(URL の中のパスはログやリファラーに漏れます)。toがあなた自身のオリジン上だと確認し、それ以外はすべて拒否する。そうすればコールバックをオープンリダイレクトに変えられません。- 下の属性で Cookie をセットする。
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)だけを守ります。非インタラクティブなトークンのエンドポイントにはゲートをかけませんし、かけるべきでもありません。
あわせて読む
- プロキシゲートを設定する:nginx の手順と、プレビューモード優先のロールアウト。
- 概要:概念と通過許可 Cookie。
- 統計:チャレンジ対パススルーを見て配線を確かめる。