名前には redirect という語が入っているため、VPN の向こう側にあるバックエンドに届かないとき、これを location ブロックに貼り付けてしまう。何も変わらず、明白な結論はこのディレクティブが壊れているか別の構文が必要だというものになる。実際に起きたことはもっと単純で、proxy_redirect はすでに返ってきたレスポンスに対して働き、この構成ではリクエストがそこまで到達していなかったのだ。

二つのディレクティブ、一つのリクエストの二つの瞬間

リバースプロキシを通る HTTP リクエストは、いくつかの判断を順番に通り、二つのディレクティブはその列の両端に位置する。一方はリクエストの行き先を選ぶ。もう一方は戻ってくるレスポンスの特定のヘッダーを編集する。

DirectiveWhen it actsWhat it can change
proxy_passBefore any bytes leave the proxyWhich upstream receives the request
proxy_redirectAfter the upstream respondsMatching Location and Refresh text

その順序が答えのすべてだ。最初の行が失敗したせいで二番目の行が決して実行されないなら、二つ目のディレクティブは設定ミスではなく、単に到達不能なのである。二番目の行をどれだけ書き換えても、それが編集すべきだったレスポンスは生まれない。一番目の行から外へ向かって診断すれば、不可解な挙動は欠けたルートか拒否された接続に落ち着く。

上流の選択は proxy_pass の仕事

NGINX が公開の入口ホストで動き、自身の VPN 経由で 10.40.0.20:8080 のアプリケーションに届くとしよう。そのアプリケーションを選ぶ断片は、既存の server ブロックの中では次のとおり:

location / {
    proxy_pass http://10.40.0.20:8080;
}

実際のスキーム、アドレス、ポートを使い、これはデプロイではなく断片として扱う。入口ホストは VPN のルート、許可された転送経路、そのサービスへの正常な戻りトラフィックも保持している必要があり、紙の上でアドレスに届くプロキシでもカーネルの合意は依然として要るのだ。

proxy モジュールのリファレンスは、上流の選択とレスポンスヘッダーの書き換えを別物として扱い、URI を含む proxy_pass の値が一致する location プレフィックスの置き換え方を変えると記している。アプリケーションが期待する URL を確認せずにパスの要素を足したり削ったりするのは、ルーティング障害に見える 404 を作る確実な方法だ。

proxy_redirect は内部アドレスを漏らすレスポンスを直す

トラフィックが流れ始めると、バックエンドが自分の内部アドレスを示す Location ヘッダーで応答することがある。そのヘッダーに従うブラウザーは公開の入口を離れ、解決できない何かに到達しようとする。意図的に設定されたレスポンス書き換えは、内部 URL を公開 URL に対応づける:

proxy_redirect http://10.40.0.20:8080/ https://app.example.com/;

両側を実際の値に置き換える。範囲は狭く、一致する Location と Refresh のヘッダーテキストは書き換わるが、HTML や JavaScript、ダウンロードしたファイルに埋め込まれた URL は書き換わらない。

アプリケーションが至る所で絶対リンクを組み立てるなら、恒久的な解決策はアプリ自身の外部 URL 設定であり、プロキシ側で書き換えルールを増やすことではない。出力が正しく見えるまでルールを足すと、誰も試していない最初のページで破綻する設定ができあがる。正しいアプリ設定がひとつあれば、その集まりごと置き換えられる。

ヘッダーの書き換えで直せないもの

隣接するいくつかの問題はリダイレクトの問題に見えるが、そうではない。上流へ送る Host ヘッダーはバックエンドの仮想ホストに一致させる必要があるかもしれない。TLS の上流は特定のサーバー名と証明書の検証を要することがあり、レスポンスヘッダーの書き換えはどちらにも効かない。

Cookie のスコープとドメイン属性はアプリケーションとその設定に属する。プロキシは一日中ヘッダーを落としたり書き換えたりしても、Cookie がブラウザーの一度も訪れていないホストに対して発行されていたために、二度目のリクエストで失敗するセッションを作り出す。その失敗は断続的に見えるので、チケットの行列で長く生き残る。Cookie のドメインを読めば、たいていは一分で調査が終わる。

認証とバックエンドの選択も、意図したアプリケーションに限定しておくこと。公開のリバースプロキシは、サーバーの VPN を通して一つのアプリケーションだけを公開すべきで、そのホストが届くすべての内部アドレスへの汎用ゲートウェイになるべきではない。単一の汎用 location が呼び出し側の求めるものを何でも転送すると、露出した一つのサービスが内部ネットワークの地図に変わる。上流を明示的に名指しし、そこに留めておくこと。

リクエストが進む順に試す

三つのことを順に確認する:NGINX が意図した location を選んだこと、バックエンドへのリクエストを完了したこと、ブラウザーが後続のリダイレクトに従ってサブリソースを読み込んだことだ。各段階にはそれぞれのログ行があり、順に読めば曖昧な 502 が具体的な所見になる。

必要な区別は、失敗した上流接続と、到達できない内部名へ送られたブラウザーの違いだ。前者はプロキシのエラーログに現れ、後者は決して届かないリクエストとして現れて、ブラウザーは解決できないホストについて不平を言う。二つの異なる問題、二つの異なるファイルであり、proxy_redirect が関わるのはそのうち一方だけだ。

関連記事