0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【GCPリフト&シフト戦記 #5】LB配下で無限リダイレクト地獄!?ASP.NET CoreとX-Forwardedヘッダーの正しい付き合い方

0
Posted at

はじめに

こんにちは。オンプレミスのC#資産をクラウドへリフト&シフトする連載の最終回(第5回)です。

セッションを外部化し、バッチを非同期キュー化し、ストレージをGCSに移行し、レガシーDLLの闇も乗り越えました。
「よし、アプリ側の準備は完璧だ。本番のGCPクラウドロードバランシング(LB)の背後にVMをぶら下げて、いざアクセス!」

……ブラウザ画面に表示されたのは、親の顔より見た ERR_TOO_MANY_REDIRECTS(リダイレクトが多すぎます) という絶望のメッセージ。さらに、アクセスログを見ると全ユーザーのIPアドレスが「ロードバランサの内部IP」になってしまっている……。

クラウド環境では、ネットワークの境界(エッジ)とアプリケーションの境界が分離されます。本記事では、GCPのロードバランサとASP.NET Coreの間に立ちはだかる「ネットワークの罠」をどうやって調教したのかを解説します。

image.png

【前提環境】

  • C# / .NET 8
  • ASP.NET Core
  • Google Cloud Platform (GCP)
  • Cloud Load Balancing (HTTPS)

1. 終わらないHTTPS無限リダイレクト地獄

【起結】⏱ 10秒まとめ:本章の結論
クラウドのLBがSSLを終端(復号)してVMへはHTTPで通信してくるため、アプリ側が「安全なHTTPSへリダイレクトしなきゃ!」と無限ループを起こします。LBが付与する X-Forwarded-Proto ヘッダーを信頼し、正しくHTTPS判定させる必要があります。

【承】🌩 課題:こんなこと困るよね。難しいよね。
オンプレミス時代、Webサーバー(IIS)には直接SSL証明書をインストールしていました。そのため、アプリは自分が「HTTPSで通信しているか、HTTPで通信しているか」を正確に把握できていました。
しかしクラウドでは、SSL証明書の管理はロードバランサ(LB)に任せるのが鉄則です。すると何が起きるか?
ユーザーはLBに対して「HTTPS」で通信しますが、LBはそれを復号し、背後のVMへは「HTTP」で通信を転送します。
ASP.NET Coreのアプリは真面目なので、「HTTPでアクセスが来た!セキュアじゃないからHTTPSのURLにリダイレクトしてあげよう!」と親切に301リダイレクトを返します。
しかし、再度LBを経由してVMに届く通信はまた「HTTP」になるため、永遠にリダイレクトのキャッチボールが始まり、ブラウザが音を上げるのです。

【転】💡 解決策:採用理由と他の選択肢
このリダイレクトループを解消するためのアーキテクチャ設計(ADR)です。

  • 選択肢A: アプリ側のHTTPS強制リダイレクト(app.UseHttpsRedirection())を外す
    • 見送り理由: 動くようにはなりますが、アプリ自身がURLを生成する際(リダイレクトやメール文面のURL生成など)に「http://〜」で出力されてしまう不具合が生じるため、根本的な解決になりません。
  • 採用案: ForwardedHeadersOptions を設定し、X-Forwarded-Proto を信頼する
    • 採用理由: ロードバランサは「本当はHTTPSで来たよ」という印として、X-Forwarded-Proto: https というHTTPヘッダーを付与してVMに転送してくれます。ASP.NET Coreに「このヘッダーが付いていたら、HTTPで受信してもHTTPSとして振る舞いなさい」と教え込む(ミドルウェアを設定する)のが、クラウドネイティブな最適解です。

【結】💻 具体例・サンプルコード

Program.cs のサービス登録と、ミドルウェアのパイプライン設定を行います。ここで超重要なのが、ミドルウェアの登録順序です。

// Program.cs (サービス登録部)

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    // LBから渡されるプロトコル情報(HTTPS)と、クライアントIP情報を信頼する設定
    options.ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor;
});

// ~~~ 中略 ~~~

var app = builder.Build();

// ========================================================
// ⚠️超重要: ミドルウェアの実行順序
// X-Forwarded ヘッダーの適用は、例外ハンドラーやHTTPSリダイレクトなど、
// "他のどのミドルウェアよりも先" に実行しなければ意味がありません!
// ========================================================

app.UseForwardedHeaders();

app.UseMiddleware<ExceptionHandlingMiddleware>(); // グローバル例外ハンドラー

if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}
app.UseHttpsRedirection();

// ... その他のルーティングなど

🔰 初心者向け解説:SSL終端(SSL Termination)とは?
「海外の取引先(ユーザー)との間に立つ優秀な通訳(ロードバランサ)」です。
取引先は英語(HTTPS=暗号化された安全な言葉)で話しかけてきますが、通訳がそれを日本語(HTTP=平文)に翻訳して、奥の部屋にいる社長(VM)に伝えます。
社長は「なんだ日本語(HTTP)じゃないか!英語で話し直せ!」と追い返そうとしますが、通訳が添えた「※これは元々英語で来たメッセージですよ(X-Forwarded-Proto)」というメモを信じるように社長を説得するのが UseForwardedHeaders の役割です。

2. IPアドレス偽装の脆弱性と「KnownNetworks」の罠

【起結】⏱ 10秒まとめ:本章の結論
クライアントの真のIPを取得するために X-Forwarded-For を有効化する際、デフォルト設定のままでは脆弱性(IP偽装)を生むか、設定が機能しません。GCPのLB構成に合わせて ForwardLimit = 1 に絞り、KnownNetworks.Clear() を行うハックが必要です。

【承】🌩 課題:こんなこと困るよね。難しいよね。
前章の設定で無事にシステムが動くようになりました。しかし、アクセスログを見ると「すべてのアクセス元のIPアドレスが、GCPのロードバランサのIP」になっています。これでは、悪意あるユーザーのIPを特定してBANすることもできません。

「じゃあ、さっきの X-Forwarded-For ヘッダーを有効にすればいいんだな!」と有効化しても、なぜかASP.NET Coreはそれを無視して、依然としてLBのIPを記録し続けます。
実はASP.NET Coreは非常に用心深く、「信頼できるネットワーク(KnownNetworks)からの X-Forwarded-For しか信じない」という安全装置が働いているのです。

【転】💡 解決策:採用理由と他の選択肢
クライアントIPを安全かつ確実に取得するためのアーキテクチャ設計です。

  • 選択肢A: GCPロードバランサのIP帯域を KnownNetworks にハードコードする
    • 見送り理由: GCPの仕様上、プロキシIPの帯域(130.211.0.0/22 など)は将来変更される可能性があり、ハードコードするとインフラ変更時にアプリが壊れる(IPが取得できなくなる)運用上の爆弾を抱えることになります。
  • 採用案: 直前の1段(ForwardLimit=1)だけを無条件に信用するよう設定をクリアする
    • 採用理由: GCPのロードバランサが「直前の1段」であることはインフラ構成上担保されています。そこで、IP帯域への依存(KnownNetworks)をクリアし、「直前のLBが付与した直近1件のIPだけを確実に信用する」設定に変更します。これにより、ZAPなどの脆弱性診断ツールによる多重ヘッダー偽装攻撃(X-Forwarded-For: 192.168.0.1, 8.8.8.8 のように複数送ってくる攻撃)も防ぐことができます。

【結】💻 具体例・サンプルコード

先ほどの ForwardedHeadersOptions に、GCP環境向けの泥臭いチューニングを書き加えます。

// Program.cs (サービス登録部)

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;

    // ★GCP環境向けのハック: IPアドレスのハードコードを避けつつ安全性を確保する

    // GCPのロードバランサが「1段構成」であることを指定。
    // 悪意あるユーザーが偽装した多重ヘッダーを送ってきても、直近の1つしか信用しない。
    options.ForwardLimit = 1;

    // 特定のIP帯域(GCPのプロキシIPなど)に依存せず、直前のLB(1段)を無条件で信用するよう
    // ASP.NET Coreのデフォルトの安全装置を解除する。
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
});

🔰 初心者向け解説:X-Forwarded-For と偽装のリスク
会社で「回覧板(HTTPリクエスト)」を回す時の話です。
本当の発信者(クライアント)が誰なのか、間に立つロードバランサが回覧板の端っこに「発信者:Aさん」とメモ(X-Forwarded-For)を書いてくれます。
しかし、悪意ある人が自分で「発信者:社長」と嘘のメモを書いて送ってくるかもしれません(ヘッダー偽装)。
ForwardLimit = 1 の設定は、「一番最後に直接手渡ししてくれた人(GCPのLB)が書いたメモ以外は、絶対に信用するな」という厳格なルールの設定です。

終わりに(連載まとめ)

全5回にわたり、オンプレミスからGCPへの「リフト&シフト」の過程で直面した泥臭い戦いの記録をお届けしました。

クラウドのインフラ(VMやマネージドサービス)はボタン一つで構築できますが、その上で動くアプリケーションが「クラウドの流儀」に調教されていなければ、真の価値は発揮できません。

  • 第1回:セッションをDBに逃がす(ステートレス化)
  • 第2回:時間起動を捨ててキュー駆動へ(バッチのモダナイズ)
  • 第3回:ディスクとパスワードを手放す(GCS / Secret Manager)
  • 第4回:レガシーDLLをねじ伏せる(AssemblyResolve)
  • 第5回:エッジの境界を正しく解釈する(ForwardedHeaders)

これらの知見が、これからクラウド移行という名の修羅場に立ち向かう皆さんの、道標(あるいは防具)になれば幸いです。最後までお読みいただきありがとうございました!

📖 単語一覧 (Glossary)

  • SSL終端 (SSL Termination): 暗号化されたHTTPS通信を、Webサーバーの直前にあるロードバランサ等で一度復号(HTTPに)する処理のこと。サーバー側のCPU負荷を下げ、証明書の管理を一元化できる。
  • X-Forwarded-Proto / X-Forwarded-For: プロキシやロードバランサを経由した通信において、「元々どんなプロトコル(HTTP/HTTPS)だったか」「元の送信元IPは何か」を後続のサーバーに伝えるための標準的なHTTPヘッダー。
  • HSTS (HTTP Strict Transport Security): Webブラウザに対して「このサイトには絶対にHTTPSで接続しろ」と強制させるセキュリティ機能。ASP.NET Coreでは app.UseHsts() で有効化する。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?