Carpe Diem

備忘録

remote_addrとかx-forwarded-forとかx-real-ipとか

背景

ECSでNginxのコンテナをプロキシとして立てたところ、APIサーバのアクセスログのクライアントIPがNginxのコンテナIPになっていたのでその修正をしたのがきっかけです。

環境

  • Nginx 1.10.2
  • Docker1.12.1

構成

f:id:quoll00:20210820054858p:plain

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

で、Proxy1Proxy2が自サービスのものであるならば

set_real_ip_from ELB;
set_real_ip_from Proxy1;
set_real_ip_from Proxy2;
real_ip_recursive on;

としておけばProxy1Proxy2は信頼された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とするロジックを採用しています。

ソース