2026年8月18日
2026年8月18日
PROXYプロトコルでクライアントIPを引き継ぐ方法(X-Forwarded-Forとの違いとNginx設定)
TCPレベルで中継するロードバランサーの後ろでは、HTTPヘッダーの X-Forwarded-For を付けられないため、PROXYプロトコルで接続元IPをバックエンドへ渡します。ロードバランサー側で PROXYプロトコルの送信を有効にし、Nginx などのバックエンド側で受信と信頼する送信元を設定すれば、WordPress のログやセキュリティプラグインに本当の訪問者IPが記録されるようになります。この記事では、仕組みの違い、HAProxy と Nginx の設定例、WordPress 側での扱い、確認方法と注意点を説明します。
X-Forwarded-ForとPROXYプロトコルの違い
ロードバランサーやリバースプロキシを挟むと、バックエンドから見た接続元は中継機器のIPになります。これを補う方法は大きく2つあります。
- X-Forwarded-For:HTTPリクエストにヘッダーを追加して接続元IPを伝える方法です。中継機器がHTTPを解釈している(L7で動いている)ことが前提です。
- PROXYプロトコル:TCP接続の最初に、接続元・接続先のIPとポートを書いた短いヘッダーを付けて送る方法です。HAProxy の開発元が仕様を公開しており、人が読めるテキスト形式の v1 と、バイナリ形式の v2 があります。HTTP以外の通信にも使えます。
PROXYプロトコルが必要になるのは、ロードバランサーがTLSを復号せずにそのまま流す(TLSパススルー)構成や、TCPモードで負荷分散している構成です。この場合ロードバランサーはHTTPの中身を見られないので、X-Forwarded-For を足すことができません。逆に、ロードバランサーでTLSを終端してHTTPとして中継しているなら、通常は X-Forwarded-For で足ります。
記事の題名にある「X-Forwarded-Forを透過させる」とは、実際には「PROXYプロトコルで受け取った接続元IPを、バックエンドの内部で X-Forwarded-For や REMOTE_ADDR として扱えるようにする」ことを指します。
構成の例
この記事では次の構成を例にします。
- ロードバランサー:HAProxy(TCPモード、TLSパススルー)、内部IP
10.0.0.5 - バックエンド:Nginx + PHP-FPM + WordPress、内部IP
10.0.0.11 - TLS証明書はバックエンドの Nginx が持つ
クラウドのロードバランサーを使う場合も考え方は同じです。たとえば AWS の Network Load Balancer では、ターゲットグループの属性で PROXYプロトコル v2 を有効にする方式が案内されています。設定場所や対応状況はサービスごとに異なり、変わることもあるため、各社の公式ドキュメントで確認してください(2026年10月現在)。
作業前の注意:送信側と受信側は同時に切り替える
PROXYプロトコルは、送る側と受ける側の設定がそろっていないと通信が成立しません。受信側だけ有効にすると、ヘッダーなしの接続は不正なデータとして拒否されます。送信側だけ有効にすると、バックエンドはTLSのハンドシェイクの前に見慣れないデータを受け取り、接続に失敗します。
本番では、検証環境で手順を確かめたうえで、次のどちらかの方法をとると停止時間を抑えられます。
- PROXYプロトコル用に別のポートを用意し、受信側を先に準備してからロードバランサーの宛先を切り替える
- メンテナンス時間を取り、両方の設定を続けて反映する
設定ファイルは変更前にバックアップしておきます。
HAProxy側:PROXYプロトコルを送る
frontend fe_https
bind :443
mode tcp
default_backend be_web
backend be_web
mode tcp
balance roundrobin
server web1 10.0.0.11:443 check send-proxy-v2
send-proxy-v2 で v2 形式、send-proxy で v1 形式を送ります。受信側が対応していれば v2 で問題ありません。ヘルスチェックの通信にもPROXYヘッダーを付ける必要がある場合は、HAProxy のドキュメントで check-send-proxy などのオプションを確認してください。
Nginx側:受け取って接続元IPに反映する
Nginx では listen に proxy_protocol を付けて受信を有効にし、realip モジュールで「誰から届いたPROXYヘッダーを信頼するか」を指定します。
server {
listen 443 ssl proxy_protocol;
server_name www.example.com;
# PROXYヘッダーを信頼する送信元(ロードバランサーのIPだけ)
set_real_ip_from 10.0.0.5;
real_ip_header proxy_protocol;
ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
root /var/www/wordpress;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
}
real_ip_header proxy_protocol; によって、$remote_addr がPROXYヘッダーに書かれた接続元IPに置き換わります。標準の fastcgi_params には fastcgi_param REMOTE_ADDR $remote_addr; が含まれているため、PHP の $_SERVER['REMOTE_ADDR'] にも訪問者のIPが入ります。PHP-FPM のソケットのパスはディストリビューションやPHPのバージョンで異なるので、環境に合わせて書き換えてください。
set_real_ip_from には、ロードバランサーのIPやサブネットだけを指定します。ここを広く書くと、第三者が偽のヘッダーで接続元を詐称できてしまいます。realip モジュールは多くのディストリビューションのパッケージに含まれていますが、nginx -V の出力に --with-http_realip_module があるかで確認できます。
さらに奥のアプリへ X-Forwarded-For として渡す
Nginx がさらに別のアプリケーションサーバーへ HTTP で中継する場合は、受け取った値をヘッダーとして付け直します。
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
$proxy_protocol_addr は、PROXYヘッダーに書かれていた接続元IPそのものを表す変数です。これが「X-Forwarded-For として透過させる」部分にあたります。
Apacheで受け取る場合
Apache httpd では、mod_remoteip の RemoteIPProxyProtocol ディレクティブでPROXYプロトコルを受け取れます。Apache 2.4 の比較的新しいリリース(2.4.31 以降)で追加された機能なので、使っているバージョンのドキュメントで対応状況を確認してください。ヘッダーを送らないヘルスチェックなどを許可したい場合の除外設定も、同じモジュールのドキュメントに記載があります。
WordPress側で必要なこと
上の Nginx の設定で REMOTE_ADDR が正しくなっていれば、WordPress 本体やセキュリティプラグイン、コメントのIP記録は特別な設定なしで訪問者のIPを使います。WordPress のコードで接続元を書き換える必要はありません。
ウェブサーバー側で設定できず、PHP で X-Forwarded-For を読むしかない場合は、リクエストが信頼できるプロキシから届いたときに限って値を採用します。条件を付けずに X-Forwarded-For を信じると、誰でもヘッダーを付けてIPを偽装でき、ログイン試行の制限などが回避されるおそれがあります。
// wp-config.php("That's all, stop editing!" の行より上)
// 信頼するプロキシから届いた場合だけ、最後に付与されたIPを採用する例
$trusted_proxies = array( '10.0.0.5' );
if ( isset( $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_X_FORWARDED_FOR'] )
&& in_array( $_SERVER['REMOTE_ADDR'], $trusted_proxies, true ) ) {
$forwarded = array_map( 'trim', explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
$client_ip = end( $forwarded );
if ( filter_var( $client_ip, FILTER_VALIDATE_IP ) ) {
$_SERVER['REMOTE_ADDR'] = $client_ip;
}
}
X-Forwarded-For は中継のたびに右へ追記されるため、左端の値は訪問者が自由に書けます。信頼するプロキシが1台なら、そのプロキシが付けた右端の値を使うのが安全です。
動作確認の方法
Nginx のアクセスログに、PROXYヘッダーの値と置き換え後の値を並べて記録すると確認しやすくなります。
log_format proxyinfo '$proxy_protocol_addr $remote_addr [$time_local] "$request" $status';
access_log /var/log/nginx/access_proxy.log proxyinfo;
nginx -t で構文を確認してから再読み込みし、外部からアクセスして自分のグローバルIPが記録されるかを見ます。
バックエンドへ直接テストする場合、curl の --haproxy-protocol オプションでPROXYヘッダー(v1)を付けた接続を作れます。
curl -vk --haproxy-protocol --resolve www.example.com:443:10.0.0.11 https://www.example.com/
この場合は送信元が curl を実行したマシンになるため、set_real_ip_from にそのIPが含まれていないとヘッダーは信頼されません。検証用の一時的な設定として扱ってください。
よくあるつまずき
- エラーログに「broken header」と出る:受信側でPROXYプロトコルを有効にしたポートに、ヘッダーなしの接続が来ています。ロードバランサー側の送信設定、ヘルスチェック、ロードバランサーを経由しない直接アクセスの順に確認します。
- TLSのエラーで接続できない:送信側だけ有効になっている可能性があります。受信側の
listenにproxy_protocolが付いているか確認します。 - IPがロードバランサーのままになる:
set_real_ip_fromの値と実際の送信元IPが一致しているか、real_ip_headerの指定があるかを確認します。 - 直接アクセスで偽装できてしまう:バックエンドはロードバランサーからの通信だけを受け付けるよう、ファイアウォールで制限します。
まとめ
- PROXYプロトコルは、TCPモードやTLSパススルーのロードバランサーで、HTTPヘッダーを使わずに接続元IPを渡す仕組みです。
- 送信側(HAProxy の
send-proxy-v2など)と受信側(Nginx のlisten ... proxy_protocol)は、そろえて切り替えます。 - Nginx では
set_real_ip_fromとreal_ip_header proxy_protocolで REMOTE_ADDR を置き換えれば、WordPress 側の変更は不要です。信頼する送信元は最小限にします。 - アクセスログに
$proxy_protocol_addrを出して確認し、「broken header」などのエラーから設定のずれを見つけます。