/home/by-natures/dev*

データ界隈で働くエンジニアとしての技術的なメモと、たまに普通の日記。

2019/06/18 AWS ELB 配下の tomcat が X-Forwarded-For を取得できない? -> 解決しました

ロードバランサー配下のWebサーバは、ロードバランサーがもともとのクライアントのリクエスト情報を保持するために X-Forwarded- というHTTPヘッダを追加します。このリクエスト元の情報を保存するヘッダはただのデファクトスタンダードでしたが、今は RFC にもなっているようです。

X-Forwarded-For - Wikipedia

AWS 環境でもこれは同じで、公式サイトや色々なブログでも、ELB配下のWebサーバは X-Forwarded-For を利用してクライアントのIPアドレスを取得できると記載されています。例えば tomcat のアクセスログでは、以下のように変更することでリクエスト元のIPアドレスを取得することができます:

# デフォルトのアクセスログフォーマット
pattern="%h %l %u %t "%r" %s %b"

# X-Forwarded-For などの、リクエスト元の情報を追加したアクセスログフォーマット
pattern="%{X-Forwarded-For}i %{X-Forwarded-Proto}i %h %l %u %t "%r" %s %b"

ここで私がハマった、、というか今でもよくわからないのですが、サーブレット側で X-Forwarded-For を取得しようとすると null が返ってきてしまいました。アクセスログで X-Fowarded-For にクライアントのIPが出力されたところで安心していて、アクセスログで出力している HTTPヘッダがサーブレットで取得できないなんて考えも及びませんでした。。サーブレットでHTTPヘッダを全て出力させましたが、 X-Forwarded-Proto はあるのに X-Forwarded-For が見当たりません。

// これが null になる、アクセスログでは取得できているのに
String clientIP = request.getHeader("X-Forwarded-For");

検索すると同じ問題を質問している AWS スレッドがありました: Forums | AWS re:Post

かいつまむと、理由はわからないが Tomcat(というかサーブレット) だと request.getRemoteAddr() でリクエスト元のIPアドレスが取得できる・・・ということです。 getRemoteAddr() はネットワーク層のリクエスト元 IP アドレスを返却するようなので、本来であれば EC2 インスタンスの手前にある ELB のIPアドレスになるはずです。上のスレッドに回答は付いていないのですが、手元のAWS環境で試すと、確かに getRemoteAddr() でリクエスト元のIPアドレスが取得できました。

今後 X-Forwarded-For でリクエスト元の IPアドレスが取得でき、 getRemoteAddr() でELBのIPアドレスが返ってこないとも限らないので、 X-Forwarded-For をチェックして null であれば getRemoteAddr() で埋めておくのが無難でしょうか。どなたかこの辺りの事情詳しければぜひ教えてください。

追記

Twitter で先輩から、tomcat の機能でそうなっているのではないか、ということで以下のモジュールを紹介してもらいました:

RemoteIpValve (Apache Tomcat 8.5.90 API Documentation)

このモジュールはクライアントのIPアドレスなどを X-Forwarded- 系のプロトコルの値と置き換えるモジュールで、今回の挙動が説明できます。実際に、Elastic beanstalk 経由で入れた tomcat の server.xml には上記モジュールが設定されていて、内部プロキシのIPアドレスも定義されていました。

<Valve className="org.apache.catalina.valves.RemoteIpValve" protocolHeader="X-Forwarded-Proto" internalProxies="10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|127\.\d+\.\d+\.\d+|172\.(1[6-9]|2[0-9]|3[0-1])\.\d+\.\d+" />