【実務・中級編】X-Forwarded-Forヘッダーの構造とクライアントIPアドレスの特定 – HTTPプロトコル・通信規格実践ガイド

こんにちは。インフラエンジニアなら誰もが一度は頭を悩ませたことがあるはずだ。「本番環境でなぜかアクセスログのIPアドレスが全部ロードバランサー(LB)のものになっている……!」と。

アプリケーションをスケールさせ、可用性を高めるためにリバースプロキシやCDN、ロードバランサーを挟むのは現代のWebアーキテクチャの定石だ。しかし、その代償として失われるのが「クライアントの素性(真のIPアドレス)」である。TCPの三方手書き(3-way handshake)を行う以上、バックエンドのWebサーバーから見えるのは、直前のホップ(つまりLB)のIPアドレスなのだから当然といえば当然だ。

この「見えなくなったクライアントIP」を取り戻すために使われるのが、非標準ながら事実上の業界標準(De facto standard)として君臨する `X-Forwarded-For` ヘッダーである。

今回は、この `X-Forwarded-For` の構造、パケットの旅路、そして実務で絶対に避けて通れない「偽装リスク」と「正しい信頼の定義」について、現場の泥臭い知見を交えて徹底的に解説しよう。

—

1. なぜ `X-Forwarded-For` が必要なのか?パケットの旅路

リバースプロキシやロードバランサーがクライアントからのリクエストを受け取り、バックエンドのサーバーへ転送する際、HTTPのレイヤーで情報を付加しなければ、サーバー側は誰が来たのかを知る術がない。

ここで、クライアントからバックエンドサーバーに到達するまでの通信のシーケンスを見てみよう。

[Client] (IP: 203.0.113.50)
│
├─ 1. HTTP Request (GET /api/v1/data)
▼
[Load Balancer / Proxy A] (IP: 198.51.100.10)
│ ※ ここでヘッダーを付与・追記する
├─ 2. HTTP Request + X-Forwarded-For: 203.0.113.50
▼
[Reverse Proxy B / CDN] (IP: 192.0.2.20)
│ ※ さらに中継する場合、末尾にプロキシ自身の接続元IPが追加される
├─ 3. HTTP Request + X-Forwarded-For: 203.0.113.50, 192.0.2.20
▼
[Backend Web Server] (Nginx / App)

このシーケンスが示す通り、リクエストが複数のプロキシを経由するたびに、経由地がカンマ区切りで連なっていくのが `X-Forwarded-For`(通称:XFF)の基本構造だ。

基本的なデータ構造

X-Forwarded-For: client, proxy1, proxy2

一番左が「インターネットの向こう側にいる本当のクライアント(Origin Client)」であり、右に向かって順番にプロキシのIPアドレスが蓄積されていく。インフラエンジニアとしてログ解析やアクセス制限を行う際に見るべきは、基本的には「一番左端のIPアドレス」ということになる。

—

2. 実務で直面する「偽装リスク」という名の罠

さて、ここからがシニアの腕の見せ所だ。若いエンジニアによくある勘違いが、「`X-Forwarded-For` の一番左を読めば完璧にクライアントIPが取れる!」というもの。これは半分正解で、半分は大間違いだ。

なぜなら、`X-Forwarded-For` はクライアント側(あるいは途中の悪意ある経由地)から自由に改ざん・偽装できるヘッダーだからだ。

もし、あなたのWebサーバーがインターネットに直接露出しており、外部からのリクエストに含まれる `X-Forwarded-For` を無条件で信頼していたらどうなるか?
攻撃者は以下のようなリクエストを簡単に送ることができる。

GET /login HTTP/1.1
Host: example.com
X-Forwarded-For: 8.8.8.8

アプリケーション側が「お、8.8.8.8(Google Public DNSのIP)からのアクセスだな」と誤認してログに記録したり、レートリミット(IP単位のアクセス制限)をバイパスされたり、果てはIP制限のある管理画面に不正アクセスされるといったセキュリティインシデントに直結する。

対策:「信頼できるプロキシ(Trusted Proxy)」の定義

このリスクを防ぐ唯一にして最大の鉄則は、「自社が管理し、信頼できるロードバランサーやリバースプロキシ以外のヘッダーを信用しない」ことだ。

リバースプロキシ(NginxやHAProxy、AWSのALBなど)は、外部から送られてきた汚染された `X-Forwarded-For` をそのままスルーするのではなく、「LBとクライアント間で確立されたTCPコネクションの直近のIPアドレス」を、正しいプロキシの仕様に基づいて強制的に上書き、または右端に追記する設定にしなければならない。

—

3. 実践:Nginxとアプリケーション層での正しい設定

では、現場でどう設定すべきか。よくある構成として、Nginxをリバースプロキシとして置き、バックエンドにアプリケーション(Python / Node.jsなど)がいるケースを考えてみよう。

Nginx側の設定 (`nginx.conf`)

Nginxでは、`proxy_set_header` ディレクティブを使って、バックエンドへ渡すヘッダーを厳密に制御する。

server {
listen 80;
server_name api.example.com;

location / {
proxy_pass http://backend_cluster;

# 既存のXFFがあればそれを引き継ぎつつ、直近のクライアントIPを末尾に連結する
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# ついでによく使う定番ヘッダーも転送
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

ここで用いている `$proxy_add_x_forwarded_for` 変数こそが肝だ。これは「クライアントからの既存の `X-Forwarded-For` ヘッダー」に「Nginxが直面している `$remote_addr`(実際の接続元IP)」をカンマ区切りで結合してくれる優れものである。クライアントが勝手に変な値を送ってきても、Nginxが正しい手前側のIPを正しくハンドリングしてくれる。

—

4. コードからの実装:Python (FastAPI / Flask) での安全なIP取得

バックエンドのアプリケーション側では、どのようにIPを拾うべきか。Pythonのフレームワークを例に、信頼できるプロキシ経由のアクセスのみからIPを正しく抽出するコードを見てみよう。

from fastapi import FastAPI, Request

app = FastAPI()

【重要】社内ネットワークやLBのIPレンジ(例: 10.0.0.0/8, 192.168.1.0/24)を定義
TRUSTED_PROXIES = {“192.0.2.10”, “198.51.100.10”}

@app.get(“/api/v1/health”)
async def health_check(request: Request):
# 直近のTCP接続元IP(NginxなどのLBのIP)
direct_client_ip = request.client.host

# X-Forwarded-For ヘッダーを取得
xff = request.headers.get(“X-Forwarded-For”)

client_ip = direct_client_ip

if xff and direct_client_ip in TRUSTED_PROXIES:
# 信頼できるプロキシからのリクエストである場合のみ、XFFの最左端(真のクライアントIP)を採用
ips = [ip.strip() for ip in xff.split(“,”)]
client_ip = ips[0]
else:
# プロキシを経由していない直接アクセス、あるいは信頼できないプロキシからの場合
# セキュリティのため、直近の接続元IPをフォールバックとして使う
client_ip = direct_client_ip

return {
“status”: “healthy”,
“resolved_client_ip”: client_ip,
“direct_remote_ip”: direct_client_ip
}

このロジックを挟むことで、万が一ロードバランサーを通さずに直接バックエンドへ叩き込まれた不正なリクエストがあっても、IPの偽装をキレイに無効化することができる。

—

5. デバッグTips:現場で使えるcurlコマンド

実務のトラブルシューティングや結合テストで、「本当に意図した通りにXFFが伝搬しているか?」を確認したいときは、`curl` を使ってダミーのヘッダーを流し込んでみると良い。

テスト1: 偽装ヘッダーをあえて送ってみる

curl -H “X-Forwarded-For: 9.9.9.9” -i http://api.example.com/api/v1/health

まともに組まれている環境であれば、この `9.9.9.9` は無視されるか、あるいはサーバー側のログに正しく「信頼できないプロキシからの入力」として弾かれる、もしくは最右端に本来の接続元IPが追加されるはずだ。

テスト2: 複数段プロキシのシミュレーション

curl -H “X-Forwarded-For: 203.0.113.50, 192.0.2.100” -i http://api.example.com/api/v1/health

複雑なCDNやWAF(Web Application Firewall)を複数経由するアーキテクチャでは、どのレイヤーでどのヘッダーが書き換わっているのか(あるいは消されているのか)を、`curl` と各段のアクセスログを突き合わせて地道に追うのが、障害解決への最も近道となる。

—

まとめ

`X-Forwarded-For` は非常にシンプルに見えて、ネットワークのトポロジーとセキュリティの境界線(Trust Boundary)の理解度がそのままコードの堅牢性に直結する奥深いテーマだ。

1. 一番左が真のクライアントIP であること。
2. しかし、ヘッダーは容易に偽装可能であること。
3. 自社管理の信頼できるプロキシ(Trusted Proxy)のスコープを明確にし、そこでヘッダーの付与・検証を必ず行うこと。

この3点を押さえておけば、どれほど複雑なマイクロサービスやマルチクラウド構成であっても、正確なアクセス制御とクリーンなログ基盤を維持することができるはずだ。日々のインフラ運用やAPI設計の現場で、ぜひ役立ててほしい。

コメント

タイトルとURLをコピーしました