背景
ECSでNginxのコンテナをプロキシとして立てたところ、APIサーバのアクセスログのクライアントIPがNginxのコンテナIPになっていたのでその修正をしたのがきっかけです。
環境
- Nginx 1.10.2
- Docker1.12.1
構成
Client -> ELB -> Nginx -> API
という構成とします。
ネットでよく見る情報
set_real_ip_from 172.31.0.0/16; real_ip_header X-Forwarded-For;
を追加する、とか
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
を追加する、とかどれがどれだか分かりにくいので1つ1つ説明していきます。
用語説明
remote_addr
アクセス元のIP。ネットワーク層の情報。
基本的に直前のIPを保持しているので、
Client ↓ ELB( remote_addr は Client ) ↓ Nginx( remote_addr は ELB ) ↓ API( remote_addr は Nginx )
となる。
Nginxのログはデフォルトだとremote_addr
を使ってクライアントIPを出力する。
X-Forwarded-For
HTTPヘッダの一つ。ロードバランサやプロキシを経由する時に送信元を判別するために利用。アプリケーション層の情報。
Client ↓ X-Forwarded-For: "" Proxy1 ↓ X-Forwarded-For: "Client" Proxy2 ↓ X-Forwarded-For: "Client, Proxy1" ELB ↓ X-Forwarded-For: "Client, Proxy1, Proxy2" Nginx
という順に追加されていく。
なので送信元を知りたければ1つ目の要素を見れば良い。
X-Real-IP
これもHTTPヘッダの一つ。ロードバランサやプロキシを経由する時に送信元を判別するために利用。アプリケーション層の情報。
X-Forwarded-For
と同じような値だけど、複数の可能性があるX-Forwarded-For
と違って1つ。
X-Forwarded-For, X-Real-IPの注意点
- IPレイヤでなくHTTPレイヤなので書き換え可能(改竄)
- HTTPSだと終端でないとヘッダは暗号化されているので取得できない
Nginxの設定
set_real_ip_from
X-Forwarded-For
は偽装可能なので、信頼できるところ以外からはreal_ip_header
を使わないようにするための設定。
この設定が無いとこのモジュールは反映されないので注意。
set_real_ip_from 172.31.0.0/16; real_ip_header X-Forwarded-For;
なら172.31.0.0/16
からのアクセスのみ書き換える。
ELBの場合、VPCのアドレス空間を指定する。
把握しているProxy(=サービス内のProxy)が複数ある場合は、複数指定可能。
real_ip_header
Nginxがremote_addr
を変更するときに使うモジュール。
real_ip_header X-Forwarded-For;
とすればELBが間にあってもちゃんとクライアントのIPをremote_addr
としてくれる。
なので、ELBを挟んでいるときはこの設定が推奨。
より詳細に言うとX-Forwarded-For
の最後のIPをクライアントIPとするが、次のreal_ip_recursive
との組み合わせによって変わる。
※IPパケットの宛先IPを書き換えるわけでなく、Nginx上の値を変えるだけ
ELB+SSLだとProxyProtocolが必要で、その場合は
real_ip_header proxy_protocol;
と設定する。
real_ip_recursive
X-Forwarded-For
のようにIPが複数ある場合、どれを利用するかの設定
off(デフォルト)
real_ip_recursive off;
と設定すると、X-Forwarded-For
の最後のIPをクライアントIPとする
Client ↓ X-Forwarded-For: "" ELB ↓ X-Forwarded-For: "Client" Nginx
の場合、Client
になる。
Client ↓ X-Forwarded-For: "" Proxy1 ↓ X-Forwarded-For: "Client" Proxy2 ↓ X-Forwarded-For: "Client, Proxy1" ELB ↓ X-Forwarded-For: "Client, Proxy1, Proxy2" Nginx
の場合、Proxy2
になる。
on
real_ip_recursive on;
と設定すると、X-Forwarded-For
の非信頼の最後のIPをクライアントIPとする。
信頼=set_real_ip_from
に載っているIP。
set_real_ip_from
に載っていないIPで、一番最後のものということ。
ELB以外にも複数のProxyがサービス内に存在するケースに有用で、
Client ↓ X-Forwarded-For: "" Proxy1 ↓ X-Forwarded-For: "Client" Proxy2 ↓ X-Forwarded-For: "Client, Proxy1" ELB ↓ X-Forwarded-For: "Client, Proxy1, Proxy2" Nginx
で、Proxy1
やProxy2
が自サービスのものであるならば
set_real_ip_from ELB; set_real_ip_from Proxy1; set_real_ip_from Proxy2; real_ip_recursive on;
としておけばProxy1
、Proxy2
は信頼されたProxyとして扱われてフィルタされ、Client
がクライアントIPとしてremote_addr
になる。
最終的にどうすればいい?
Client -> ELB -> Nginx -> API
という構成であれば
server { listen 80; listen [::]:80; set_real_ip_from 10.10.0.0/16; # 信頼できるアドレス空間を指定。 real_ip_header X-Forwarded-For; # remote_addrを書き換え。 real_ip_recursive on; # 必要であれば。 location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header Connection ""; proxy_set_header X-Real-IP $remote_addr; # x-real-ipにクライアントIPを設定。APIへ渡す。 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # X-Forwarded-For に直前のProxy(=ELB)を追加 } }
でOKです。
API側のチェックは?
x-real-ip ↓ x-forwarded-for ↓ remote_addr
の順にチェックすると良いと思います。存在すればそれを使い、無ければ次の要素をチェックする感じで。
なぜNginxはX-Forwarded-For
の最後のIPをクライアントIPとするのか
最初に「送信元を知りたければ1つ目の要素を見れば良い」と書きましたが、加えて「X-Forwarded-ForはL7の情報なので改竄が可能」とも書きました。
例えばClientが偽装してX-Forwarded-For: X, Y, Z
といった情報をつけると
Client ↓ X-Forwarded-For: "X, Y, Z" ELB ↓ X-Forwarded-For: "X, Y, Z, Client" Nginx
となり、単純に1つ目を使うとX
という間違ったクライアントIPを残すことになります。
これを防ぐため、NginxはX-Forwarded-Forの最後のIPをクライアントIPとするロジックを採用しています。
ソース
- HTTPクライアントの接続元IPアドレスを知る | 3.1415.jp
- 【Play Framework】 ELB 経由のアクセス元IPアドレスを取得する | DevelopersIO
- ELB経由のnginxでアクセス元IPアドレスをアクセスログの$remote_addrとして書くようにする - Qiita
- [Sy] nginx(リバースプロキシ)+node.jsでクライアントのIPがすべて127.0.0.1になってしまう場合の対処 | Syntax Error.
- reverse proxy - nginx real_ip_header and X-Forwarded-For seems wrong - Server Fault