はじめに
こんにちは。オンプレミスのC#資産をクラウドへリフト&シフトする連載の最終回(第5回)です。
セッションを外部化し、バッチを非同期キュー化し、ストレージをGCSに移行し、レガシーDLLの闇も乗り越えました。
「よし、アプリ側の準備は完璧だ。本番のGCPクラウドロードバランシング(LB)の背後にVMをぶら下げて、いざアクセス!」
……ブラウザ画面に表示されたのは、親の顔より見た ERR_TOO_MANY_REDIRECTS(リダイレクトが多すぎます) という絶望のメッセージ。さらに、アクセスログを見ると全ユーザーのIPアドレスが「ロードバランサの内部IP」になってしまっている……。
クラウド環境では、ネットワークの境界(エッジ)とアプリケーションの境界が分離されます。本記事では、GCPのロードバランサとASP.NET Coreの間に立ちはだかる「ネットワークの罠」をどうやって調教したのかを解説します。
【前提環境】
- 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として振る舞いなさい」と教え込む(ミドルウェアを設定する)のが、クラウドネイティブな最適解です。
- 採用理由: ロードバランサは「本当は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のように複数送ってくる攻撃)も防ぐことができます。
- 採用理由: GCPのロードバランサが「直前の1段」であることはインフラ構成上担保されています。そこで、IP帯域への依存(KnownNetworks)をクリアし、「直前のLBが付与した直近1件のIPだけを確実に信用する」設定に変更します。これにより、ZAPなどの脆弱性診断ツールによる多重ヘッダー偽装攻撃(
【結】💻 具体例・サンプルコード
先ほどの 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()で有効化する。
