Forwarded ヘッダの使用

従来、HTTPリバースプロキシは非標準ヘッダを使って、ユーザのIPアドレスと他のリクエストプロパティについてアップストリームサーバに通知します。

X-Forwarded-For: 12.34.56.78, 23.45.67.89
X-Real-IP: 12.34.56.78
X-Forwarded-Host: example.com
X-Forwarded-Proto: https

NGINXは$proxy_add_x_forwarded_for 変数を提供して 到着するX-Forwarded-For ヘッダに$remote_addrを自動的に追加します。

RFC 7239 は新しい Forwarded ヘッダを標準化して、この情報をもっと体系化された方法で伝送します:

Forwarded: for=12.34.56.78;host=example.com;proto=https, for=23.45.67.89

Forwardedの主な利点は拡張性です。例えば、X-Forwarded-Forを使うと、“最後から2番目のIPアドレスを取得するが、リクエストが 10.0.0.0/8 から来た場合のみ” などのハードコードされた規則が無いと、どれが信頼するIPアドレスか知ることができません。一方で、Forwardedを使うと、信頼できるフロントエンド プロキシは自分自身を識別するための秘密トークンを含めることができます。

Forwarded: for=12.34.56.78, for=23.45.67.89;secret=egah2CGj55fSJFs, for=10.1.2.3

NGINXでの使用方法

NGINXは$proxy_add_x_forwarded_forのように$proxy_add_forwarded変数を提供しませんが、configで部分的に エミュレートすることができます:

map $remote_addr $proxy_forwarded_elem {
    # IPv4 addresses can be sent as-is
    ~^[0-9.]+$          "for=$remote_addr";

    # IPv6 addresses need to be bracketed and quoted
    ~^[0-9A-Fa-f:.]+$   "for=\"[$remote_addr]\"";

    # Unix domain socket names cannot be represented in RFC 7239 syntax
    default             "for=unknown";
}

map $http_forwarded $proxy_add_forwarded {
    # If the incoming Forwarded header is syntactically valid, append to it
    "~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";

    # Otherwise, replace it
    default "$proxy_forwarded_elem";
}

次に、proxy_pass ブロックで、次のように書くことができます:

proxy_set_header Forwarded $proxy_add_forwarded;

そこに追加のパラメータを追加することもできます:

proxy_set_header Forwarded "$proxy_add_forwarded;proto=$scheme";

警告

ticket #1316 が解決されるまで、この解決法は複数の到着するForwardedヘッダをサポートしません。例えば、リクエストが次のものを持つ場合:

Forwarded: for=1.2.3.4
Forwarded: for=5.6.7.8

上で定義された$proxy_add_forwardedは以下を生成します:

Forwarded: for=1.2.3.4, for=9.10.11.12

これは、複数の到着するヘッダを正しくjoinする$proxy_add_x_forwarded_forとは対照的です。

無効なヘッダの処理

上記のmap $http_forwarded内の巨大な regexは、構文的に有効な全てのForwardedヘッダと一致します。これにより、NGINXが誤った形式のヘッダを盲目的に追加しないようにします。そうでなければ、外部の攻撃者が以下のような何かを送信できます:

Forwarded: for=injected;by="

そして、NGINXは次のものを生成します:

Forwarded: for=injected;by=", for=real

アップストリーム サーバがこのようなForwardedを解析する方法に応じて、for=real要素が表示される場合とされない場合があります。(X-Forwarded-Forとは異なり、カンマは引用符で囲まれた有効な文字列で発生する可能性があるため、カンマで分割することができません)。

アップストリームがこれを正しく処理することが分っている場合、構文チェックを落として無効な要素を渡すことができます。これは相互運用性、アップストリーム ログなどに役立つことがあります:

map $http_forwarded $proxy_add_forwarded {
    ""      "$proxy_forwarded_elem";
    default "$http_forwarded, $proxy_forwarded_elem";
}

X-Forwarded-*との共存

上記の解決法は、従来のX-Forwarded-* ヘッダを新しいForwarded 形式に “アップグレード” することができません。状況に応じて、おそらく次のいずれかを引き継ぐ必要があります:

proxy_set_header Forwarded $proxy_add_forwarded;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

あるいは、アップストリームを混乱させないために、それらを積極的に削除します:

proxy_set_header Forwarded $proxy_add_forwarded;
proxy_set_header X-Forwarded-For "";
inserted by FC2 system