はじめに:NATの向こう側で「あの日のクライアントIP」を見失う絶望
夜中の3時、PagerDutyの鋭いアラート音で叩き起こされる。
「不正アクセス検知システムが、特定のプライベートIPからのリクエストをすべてブロックし続けている。しかも、そのIPがなぜかAWSのNAT GatewayのグローバルIPになっているせいで、社内からの正当なAPIリクエストまで巻き込んで弾かれている!」
クラウドインフラの設計に携わるエンジニアなら、誰もが一度はこの「送信元IP消失の呪い」に頭を抱えたことがあるはずです。
私たちはセキュリティと可用性を担保するため、プライベートサブネットにバックエンドのAPIサーバーを配置し、インターネットや外部パートナーへのリクエストはNAT Gateway(あるいはGCPのCloud NAT)を介してルーティングします。このとき、L3/L4のネットワーク層において、クライアントのプライベートIPアドレス(例: 10.0.1.100)は、NAT Gatewayの持つElastic IP(例: 203.0.113.50)に送信元NAT(SNAT / Source NAT)によって書き換えられます。
結果として、バックエンドのアプリケーションサーバーやロードバランサーに届くパケットの送信元IPは、すべて「NAT Gatewayの顔」になってしまいます。「これでは誰がリクエストを送ってきたのか分からない。アクセスログ監査も、IPベースのレートリミットも機能しないじゃないか!」――そう嘆く君に、シニアSREの私が教えよう。
この難題をエレガントに解決する切り札が、L7層の X-Forwarded-For ヘッダーであり、そしてL4層における Proxy Protocol なのだ。今回は、パケットの挙動から具体的なコード実装、そして現場のトラブルシューティングまで、徹底的に解説しよう。
—
1. NATの宿命と、IPアドレスが隠蔽されるメカニズム
まずは、なぜ送信元IPが消えてしまうのか、そのネットワークの裏側をパケットの旅になぞらえておさらいしておこう。
プライベートサブネット(例: 10.0.0.0/16)にいるコンテナやEC2インスタンスから、外部の宛先へ向けたTCPコネクションが張られるとする。
1. パケットの生成: クライアント(10.0.1.50:54321)が、外部API(198.51.100.20:443)宛てのSYNパケットを送信する。
2. NATの介入: パケットがルートテーブルに従ってNAT Gatewayを通過する際、ルーター(SNAT機構)はIPヘッダーの送信元IPアドレスを、自身が持つグローバルIP(203.0.113.10)に書き換える。同時に、TCPポート番号もポート多重化(NAPT)のために書き換えられることがある。
3. 宛先への到達: 宛先サーバーに届くパケットの送信元は 203.0.113.10:60000 のように見え、元のプライベートIPは完全に隠蔽される。
この仕組みはセキュリティ上(プライベートIPの秘匿)極めて重要だが、「リバースプロキシやロードバランサーの背後で動くマイクロサービスが、真のクライアントIPを知りたい」というユースケースにおいては、最大の壁となる。
—
2. IP伝達の2大アプローチ:L7層の「HTTPヘッダー」 vs L4層の「Proxy Protocol」
真のクライアントIPをバックエンドに伝えるアプローチには、大きく分けて2つのレイヤーが存在する。それぞれの特徴と使い分けを理解することが、アーキテクチャ設計の第一歩だ。
① L7層アプローチ: X-Forwarded-For (XFF) ヘッダー
HTTP/HTTPSなどのWebトラフィックであれば、リバースプロキシ(AWS ALBやNginxなど)がリクエストを受け付けた際、HTTPヘッダーに元のクライアントIPを挿入してバックエンドへ転送するのが最もポピュラーだ。
- メリット: アプリケーションコードから容易に取得できる。HTTPの標準的なデファクトスタンダード。
- デメリット: L7(HTTP)限定であること。TCPストリームやTLSのまま中継するTCPプロキシ(NLBなど)では利用できない。
② L4層アプローチ: Proxy Protocol(v1 / v2)
HTTPのようなアプリケーション層のプロトコルに依存せず、TCPコネクションの確立直後に「元の接続元・宛先IPとポート情報」をバイナリまたはテキストのヘッダーとしてねじ込む仕組みが Proxy Protocol だ。Haproxyが発案し、現在ではAWS NLBやGCP TCP Proxy、Nginxなどが幅広くサポートしている。
- メリット: TCPレベルで動作するため、TLSパススルーやデータベース接続、gRPC、WebSocketsなど、あらゆるプロトコルでクライアントIPを保存できる。
- デメリット: バックエンドのサーバー(またはアプリケーション)側がProxy Protocolを解釈できる設定になっていなければ、不正なパケットとみなされてコネクションが切断される。
—
3. Proxy Protocolのデータ構造と通信シーケンス
現場でトラブルが起きたとき、パケットキャプチャ(tcpdumpやWireshark)を眺めるスキルはSREの必須科目だ。Proxy ProtocolがTCPストリームの中でどのように振る舞うのか、その構造を覗いてみよう。
Proxy Protocolには、人間が読めるテキスト形式の v1 と、効率的なバイナリ形式の v2 が存在する。実務ではパフォーマンスと堅牢性の面から v2 を使うのがデファクトだ。
Proxy Protocol v2 のシグネチャとパケット構造
Proxy Protocol v2のTCPストリームは、必ず以下の12バイトの固定シグネチャから始まる。
\x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
その後に、コマンド(LOCALかPROXYか)、プロトコルファミリー(IPv4/IPv6/UNIX)、そしてアドレス長を示すヘッダーブロックが続き、最後に実際のクライアントIPとポート番号が格納される。
[ TCP 3-way Handshake 完了 ]
↓
[ プロキシサーバー ] -- (Proxy Protocol v2 ヘッダー + クライアント情報) --> [ バックエンドサーバー ]
↓
[ 通常のTCPデータ通信開始 ]
もし、バックエンド側(例:Nginxや自製アプリ)がProxy Protocolを受け付ける設定になっていないのに、前段のNLB等がProxy Protocolを有効にしてパケットを送ると、最初の12バイトがHTTPリクエストやアプリケーションプロトコルとして解釈できず、Invalid protocol エラーで即死する。ここが現場で最も多いハマりどころだ。
—
4. 実装・設定のハンズオン
理論はこのくらいにして、実際にAWS環境やコンテナネットワークを想定した設定・コードの実例を見ていこう。
パターンA: AWS ALB + X-Forwarded-For (HTTP/HTTPS APIのケース)
AWSのApplication Load Balancer (ALB) は、デフォルトで X-Forwarded-For ヘッダーを付与してバックエンドのターゲットグループ(ECSやEC2)へ転送する。Node.js (Express) でこのヘッダーを安全に取得するコードを見てみよう。
const express = require('express');
const app = express();
// ALBなどの信頼できるプロキシの背後にあることをExpressに伝える
// これにより req.ip が X-Forwarded-For の先頭IPを正しく指すようになる
app.set('trust proxy', true);
app.get('/api/v1/status', (req, res) => {
// req.ip または req.headers['x-forwarded-for'] からクライアントIPを取得
const clientIp = req.ip;
const forwardedFor = req.headers['x-forwarded-for'];
console.log(`Received request from Client IP: ${clientIp}`);
console.log(`Full X-Forwarded-For chain: ${forwardedFor}`);
res.json({
status: "healthy",
your_ip: clientIp,
message: "Successfully captured your real IP through NAT/ALB!"
});
});
app.listen(3000, () => {
console.log('API Server running on port 3000');
});
パターンB: AWS NLB + Proxy Protocol v2 + Nginx (TCP/gRPCのケース)
ネットワークロードバランサー (NLB) を使ってTCPトラフィックを処理し、背後のNginxでProxy Protocol v2を受け取る設定だ。
1. Nginxの設定ファイル (nginx.conf)
バックエンドのNginxでProxy Protocolを有効化するには、listen ディレクティブに proxy_protocol を付与する。また、リアルIPをログやFastCGIに正しく引き渡すために set_real_ip_from を設定する。
http {
# 信頼するプロキシ(AWS NLBのVPC内プライベートIPレンジ等)を指定
set_real_ip_from 10.0.0.0/16;
# Proxy Protocolヘッダーから実際のクライアントIPを $remote_addr に上書きする
real_ip_header proxy_protocol;
server {
listen 80 proxy_protocol; # ポート80でProxy Protocolを受け付ける
listen 443 ssl proxy_protocol; # HTTPS(パススルーまたは終端)
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
location / {
proxy_pass http://backend-app:8080;
# バックエンドのアプリケーションへも本当のIPとプロトコルを伝える
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
—
5. 現場のトラブルシューティング:パケットとログの追い方
「設定したはずなのに、アプリのログを見ると相変わらずNAT GatewayのIPが表示される……」
そんな絶望的な状況に直面したとき、シニアSREが取るべき黄金のデバッグ手順を伝授しよう。
Step 1: tcpdumpでパケットの中身を直接覗く
バックエンドサーバーのインターフェースで、実際にどのようなバイナリが流れてきているのかを tcpdump でキャプチャする。
# インターフェース eth0 のポート8080への流入パケットをHEX(16進数)付きでキャプチャ
sudo tcpdump -i eth0 -nnvvXS tcp port 8080
チェックポイント:
通信の最初の数バイトに、先ほど紹介したProxy Protocol v2のシグネチャ(0d 0a 0d 0a 00 0d 0a 51 55 49 54)が含まれているか確認する。含まれていない場合、前段のロードバランサー(ALBやNLB)側でProxy Protocolが無効になっている可能性が高い。
Step 2: セキュリティグループとルーティングの確認
NAT Gatewayを経由するトラフィックは、往復のルーティングが複雑になりがちだ。
- ALBやNLBのターゲットグループの設定で、Proxy Protocolのチェックボックスが正しくオンになっているか?
- バックエンドサーバーのセキュリティグループ(Security Group)が、ロードバランサーからのインバウンドトラフィックを許可しているか?(クライアントのセキュリティグループではなく、LBのSGが送信元になる点に注意)
—
おわりに:インフラとアプリの境界線を融かす設計美
NAT Gateway配下のクライアントIP保存は、単なる「設定項目の有無」の問題ではない。L3/L4のネットワーク層と、L7のアプリケーション層がどのように連携しているか、そのパケットの全貌をイメージできるかどうかが、インフラエンジニアの腕の見せ所だ。
X-Forwarded-For でスマートにHTTPリクエストを彩るか、あるいは Proxy Protocol v2 で泥臭くTCPのレイヤーから真実のIPを救い出すか。
システム要件に合わせ、最適なレイヤーでルーティングとプロトコルを選択できるようになれば、夜中に突然鳴り響くアラートの音色も、少しは心地よいBGMに聞こえてくるはずだ――冗談はさておき、あなたの設計するクラウドネットワークが、安全かつ透明性の高いものでありますように。
コメント