利用者は VPN を切断し、社内サイトで 403 に遭遇し、プロキシが壊れていると報告する。そのページを生み出しうるのは、オリジンが要求を拒否したのか、フォワードプロキシがトンネルを開くのを拒否したのか、プロキシが自前の資格情報を求めたのか、という三つの場合だ。どれも別の問いへの正しい答えであり、それらを一つの一般的な障害ページに潰すと、手元にある唯一の証拠が失われる。拒否を行ったコンポーネントで分類し、次に各コンポーネントが実際に使うネットワーク経路を比較する。

403 を生み出したコンポーネントを特定する

HTTP ステータスコードはそれを送った側のものである。オリジンが 403 を返すとき、要求を解析し、呼び出し元がリソースを持たない可能性があると判断しているが、これは認可の答えであり、転送の問題ではない。フォワードプロキシが CONNECT に 403 を返すとき、トンネルの構築そのものを拒否しており、上流には一切接触していない。407 を返すときは自前の資格情報を求めており、アプリケーションのログインとは何の関係もない。

What you observeWhat it means
Origin replies 403The origin understood the request and denied it
Proxy refuses CONNECT with 403The proxy declined to open that tunnel
Proxy replies 407Proxy authentication is required first
Gateway replies 502 or 504Upstream failure or timeout, not a permission decision

RFC 9110 はプロキシ固有のチャレンジを含め、これらの役割を定義しているので、本文を読む前に応答自身のステータスと Proxy-Authenticate ヘッダーを読むこと。親切な HTML エラーページは、どのコンポーネントが書いたかを通常は隠してしまう。

CONNECT トンネルがバイト中継から隠すもの

プロキシが CONNECT に 200 を返した後、それはバイトを中継しており、ブラウザはオリジンとの TLS セッションを自分で保持する。したがってバイトを中継するプロキシは、そのセッション内の暗号化された 403 を読めず、拒否の作成者になることは決してない。トンネルが開く前の拒否はまったく別の事象であり、一段下の層で生じる。

TLS を終端するリバースプロキシは、TLS の境界を所有し、HTTP を解析し、応答を正当に書き換えたり傍受したりできる点で立場が異なる。その能力は構成上の選択であり、プロキシ全般の性質ではなく、アプリケーションのアクセス方針を迂回する権限を与えるものでもない。応答を読むことは能力であり、呼び出し元が何を見てよいかを決めるのは依然としてオリジンの仕事である。

実務上の帰結は、403 を解釈する前に三つの形態のどれを動かしているかを知らなければならないということだ。ストリームプロキシに HTTP 検査ルールを足しても可視性は得られず、バイト中継にオリジンの拒否を説明させようとすれば、間違ったログを見に行くことになる。

プロキシの出口経路とブラウザのトンネルアドレスを比較する

VPN に接続したブラウザが、別のプロキシサーバーを自動的にそのトンネルへ移すわけではない。オリジンが見るのは プロキシの出口アドレス であり、プロキシが今もローカルの既定ルートから出ているなら、保護されたサイトは信頼するよう設定されたことのないアドレスを見ることになる。ノート PC 上の内側の VPN アドレスはそのオリジンには見えない。これが、動作しているトンネルでも他の全員には正しく振る舞うサイトから 403 が出る、最もよくある理由である。

アドレスが混同されないよう、二つの経路を別々に試す。

curl -sS -o /dev/null -w 'status=%{http_code}\n' --noproxy '*' https://198.51.100.20/private
curl -sS -o /dev/null -w 'status=%{http_code}\n' -x http://203.0.113.10:3128 https://198.51.100.20/private

最初の要求はオリジンが自分のアドレスに何と答えるかを示し、二番目はプロキシのアドレスに何と答えるかを示す。一致する 403 の組はオリジンの認可ルールを指し、200 の後に 403 が続くならプロキシ経路を指す。VPN プールが含まれていると決めつけず、どの送信元アドレスをルール群が実際に許可するのかをオリジンの所有者に尋ねる。

境界を隔離する要求で各境界を試す

アプリケーション上の理由では拒否できないものをプロキシに求めることで、プロキシ層を隔離する。プロキシが到達を許されたホストへの CONNECT を、出力を捨てて実行すれば、トンネルの受け入れがそもそも機能するかが分かる。

curl -sSv -o /dev/null -x http://203.0.113.10:3128 https://198.51.100.20/ 2>&1 | grep -E 'CONNECT|HTTP/1.1|Proxy-Authenticate'

CONNECT に 200 が出て、その後に TLS ハンドシェイクが続くなら、プロキシの受け入れは正常で、後の 403 はオリジンから来たことになる。407 が出るなら --proxy-user でプロキシ資格情報を渡し、サイトについて何か結論を出す前に繰り返す。CONNECT 行に 403 が出るなら、調べるべき境界はプロキシ自身の許可リストであり、アプリケーションは関与していない。

資格情報はシェル履歴や貼り付けた記録に残さない。コマンドラインに入力したプロキシのパスワードは他のローカル利用者から見え、資格情報付きで取得した CONNECT のトレースは、トレースがないより悪い。

ページを書き換えるのではなく、判定を読める形で残す

どのコンポーネントが拒否したか分かったら、その答えを保存する。オリジンの 403 を 504 タイムアウトと付け替えたり、成功ページに置き換えたりするリバースプロキシは、認可の判断を可用性の物語の裏に隠し、次の対応者を間違ったチームへ送る。独自のエラーページは、ステータスを保ち文脈を加えるなら問題ない。

キャッシュにも同じ注意が必要である。RFC 9111 は共有キャッシュが応答を再利用してよい方法を定めており、ある呼び出し元のために生じた拒否を、許可であるかのように別の相手へ渡してはならない。拒否応答を保存するなら、私的で範囲を限定したままにし、上流のトラフィックを減らすためだけに公開の鮮度ルールを加えてはならない。

コンポーネント、それが生み出したステータス、オリジンが見た送信元アドレスを名指しする文書化された判定で締めくくる。正確な要求、有効なプロキシ設定、時刻を記録し、次の報告を記憶から再導出するのではなく同じ証拠と突き合わせられるようにする。その記録は次の報告に数秒で答え、同じ 403 が再び VPN 障害と診断されるのを止める。

関連記事