誰かが公開 HAProxy ホストを社内アプリに向け、なぜブラウザが追従できないのかと尋ねる。プロキシは VPN 経由でアプリに到達し、リダイレクトは Location ヘッダーを返し、ブラウザはトンネル内にしか存在しないアドレスを試す。外から見るとページはただ固まる。どちらの側が二番目の接続を開くかが論点であり、それがリダイレクトとバックエンドを分ける。

設定では似て見える二つの経路

リダイレクトは応答でやり取りを終える。クライアントは Location を読み、自分がいる場所から新しいリクエストを始めるが、それは自分が一度も制御していないネットワークだ。プロキシはもう関与しない。その後のすべてはクライアント自身の DNS、経路、信頼に依存するため、結果はユーザーがブラウザを開いた場所によって変わる:

GET /app        client -> proxy
302 Location    proxy  -> client   (http://10.40.0.20:8080/)
GET /app        client -> 10.40.0.20:8080   client network, no route

プロキシのバックエンドは二番目の区間をサーバー側に保つ。プロキシはすでに持つ経路でそれを開き、クライアントは一つの公開名と話し続ける。トンネルが落ちれば、クライアントは追従できない URL ではなくエラーを受け取る。この違いが障害を診断可能にするのは、エラーがログのある場所に現れるからだ:

GET /app        client -> proxy
GET /app        proxy  -> 10.40.0.20:8080   over the VPN
response        proxy  -> client

HAProxy の リダイレクトのドキュメント はまさにこの線を引く。クライアントにリダイレクトで答えることは、リクエストをバックエンドへ転送することとは別の行為だ。二つのディレクティブは設定ファイルで数行しか離れておらず、一方はリクエストを終わらせ、他方はプロキシが所有する二番目のリクエストを始める。

ブラウザが必要とし、サーバーの VPN が与えないもの

ブラウザ自身のリクエストには、プライベート名への DNS 応答、そのアドレスへの経路、待ち受けサービスへ到達する許可、そしてそのサービスが提示する証明書への信頼という、持っていないかもしれない四つが必要だ。プロキシのホストはトンネルを通じて四つすべてを持つ。どれも、そのトンネルに一度もいなかったクライアントには移らない(トンネルはホストの性質であり、URL の性質ではないからだ)。

この差が「ほぼ動く」という症状を説明する。最初のリクエストは成功し、アドレスバーが変わり、その後に接続が固まるか失敗するが、その間プロキシはずっと健全だ。プロキシの設定に壊れたところはなく、調整しても 10.40.0.20 への経路を持たないクライアントの助けにはならない。クライアントが経路を得るか、プロキシが代わりに接続するしかない。

プロキシだけがアプリに到達できる場合

リダイレクトのアクションを外し、そのリクエストをプライベートアドレスに向けたバックエンドへ送る。フロントエンドは公開名を保ち、バックエンドがリクエストの実際の行き先を決めるので、クライアントの経路は何も変わらない。プロキシが宛先を説明するのをやめ、自身が宛先になるからだ。内部ポート 8080 で意図的に待ち受けるアプリなら、サーバーブロックは短い:

backend private_app
    mode http
    option httpchk GET /health
    server app1 10.40.0.20:8080 check

そのブロックは、10.40.0.20 がプロキシホスト自身から、ここではデフォルトルートではなく VPN インターフェース経由で到達可能でなければならないという、ルーティングテーブルについての記述として読む。check キーワードはその経路が何をするかだけを報告し、ヘルスパスはアプリに存在しなければ、すべてのプローブがサーバーをダウンと印付ける。合格したチェックはクライアントについて何も証明せず、バックエンドを誰が使えるかを決めるポリシーを置き換えない。

この断片は完全なフロントエンドではない。リスナー、認証、許可されたホストとパスの選択、そしてアプリに誰が到達できるかを決めるルールを保つ。バックエンドのプロトコルも合わせるべきで、HTTPS バックエンドは証明書の検証とサーバー ID の設定を必要とし、その検証を省くとプライベート接続が未検証の接続になる。

勝利を宣言する前に、両方向の戻り経路を確認する。アプリは今やプロキシからの接続を見るので、別段の手当てをしない限り、そのログとクライアントアドレスのメタデータはプロキシを記述する。識別情報と転送アドレスの扱いを決め、想定するのではなく決定を試す。

アプリ自身のリダイレクトは依然として内側を指す

最初のリクエストを転送するのは統合の半分にすぎず、アプリは絶対リダイレクト、ハードコードされたアセット URL、内部で知る名前に限定された Cookie を出しうる。そのどれもがブラウザを到達できないアドレスへ戻す。内部名を指す Location ヘッダーは元の問題と見分けがつかず、だからプロキシの障害と誤診される。

アプリの文書化された外部 URL とそのプロキシ設定を定め、Host ヘッダーを仮想ホストと証明書の期待に合わせる。内部名に限定された Cookie は公開名では戻ってこず、続くサインインのループは認証の不具合として読まれる。まず外部 URL を変え、きれいなブラウザプロファイルで再試験し、半分だけ移行した設定が成功するログインと即死するセッションを生むと予想する。

転送メタデータは指定されたプロキシからのみ信頼する。どのクライアントでも設定できるヘッダーを認可値にしてはならず、さもないと呼び出し側が他人の識別情報を主張する手段になる。このルールはリダイレクトの論点とは独立で、その後のどの再設計にも耐える。

実際にどちらの設計なのかを決める

すべての利用者がすでに認可された VPN アクセスを持つなら、プライベート URL へのリダイレクトは意図的な設計になりうる。その要件は、二つの設計がまったく異なる利用者層に仕えるのだから、想定せず明示的に確認する。片方はホテルのネットワークからはまったく動かず、もう片方は公開 URL を読み込める誰にでもアプリをさらす。その区別は設計メモに書くべきものだ。

利用者のネットワークにあるマシンからチェックを実行し、最初の応答だけでなくブラウザのすべてのホップを追う。アドレスバーと後続の各リクエストに注意する。アセットタグの絶対 URL が一つあるだけで最初のホップが通った流れは壊れるので、サインインは公開名で機能しなければならず、拒否すべきリソースは拒否されたままでなければならない。

リバースプロキシを公開することは別のアクセス判断であり、プロキシ自身の VPN を公開 URL を読み込める誰にでも透過的に広げることではない。図では両者は似て見え、誰を受け入れるかで異なる。その問いを配備後に決めることが、社内アプリを偶然に公開アプリへ変える道だ。

どちらを建てたのか、なぜかを書き留める。設定に触れる次の人はもう片方を前提にする。便利のために足したリダイレクトが一つあるだけで元の障害は再現し、症状は今しがた直したものとまったく同じに見える。

関連記事