【nginx】CloudFront, ALBどちらからのリクエストでも適切なremote_addrを取得する【php-fpm】

本記事の趣旨

とあるアプリケーションの前段にCloudFrontを挟む作業をしていたのですが、CloudFrontが挟まるとnginx側でシンプルに X-Forwarder-forを使用してuser側のIPを取得することができませんでした。

また、ごく一部にALB越しでのアクセスが存在しており、あとは無停止でalbからcloudfrontへ移行する必要があったので、nginx側でcloudfront越しのIPとalb越しのIPどちらのIPも常に適切なものを所得できる必要がありました。

一部のブログではset_real_ip_fromディレクティブに 0.0.0.0/0を挿入し、すべてのIPを信頼するといったやり方が紹介されていますが、 X-Fowarded-Forは偽装可能なので今回は使用を避けました。 (もちろんアプリケーションの仕様によってはこちらを採用しても良いと思います。)

そこで、cloudfrontのカスタムヘッダである CloudFront-Viewer-Addressを使用しクライアントののIP:Portを取得し、そのままだとport番号まで文字列に含まれているので一部加工してからアプリケーションへ渡すことにしました。

また、alb cloudfrontともに対応するために該当ヘッダが存在するかどうかで最終的に渡すipを決めています。

前提

X-Forwarded-forとかそのあたりの使用は既に理解しており、その上でタイトルのことを実現したい人に向けて書いてます。
X-Fowarded-For周りに関しては下記ブログがわかりやすいのでおすすめです。 こんばんは、X-Forwarded-For警察です - エムスリーテックブログ

構成

Screenshot_2022-04-27 17.23.48_iQP5qr.png

解決方法

CloudFrontのカスタムヘッダを使用する

Amazon CloudFront はクライアント IP アドレスと接続ポートヘッダーのサポートを追加 2021年のアプデでCloudFrontのカスタムヘッダとして、リクエスト元のIPとPortを挿入できるようになりました。 が、このままだとportも記述されており、IPだけがほしいのでluaで取り出します。

# server ディレクティブの中に記述
    set $real_ip "";
    set_by_lua_block $real_ip {
      local cfip = ngx.req.get_headers()["CloudFront-Viewer-Address"]
      n = string.find(cfip, ":")
      ip = string.sub(cfip, 1, n-1)
      return ip
    }

本来であればこの $real_ipreal_ip_headerに文字列として挿入したいのですが、syntaxとしてサポートしていないので別の方法を考えます。 今回はphp-fpmを使用しているので fastcgi_paramとして REMOTE_ADDR$real_ipで上書きしてあげます。

fastcgi_param REMOTE_ADDR $real_ip;
# や
fastcgi_param HTTP_X_FORWARDED_FOR $real_ip;
# や
fastcgi_param HTTP_X_REAL_IP $real_ip;
# など

参考 CloudFront=>ELB=>EC2(nginx) で、接続元 IP アドレスを取得する - Qiita

CloudFront, ALBどちらでも正しいクライアントIPを取得する

httpディレクティブ内でnginxの変数 $remote_addrにIPが入るように設定を行います。

    set_real_ip_from  <VPC CIDRレンジとか>;
    real_ip_header    X-Forwarded-For;

次に、例のluaスクリプト部分でカスタムヘッダが存在するかどうかで $real_ipの中身を決めます。

# server ディレクティブの中に記述
    set_by_lua_block $real_ip {
      if (ngx.req.get_headers()["CloudFront-Viewer-Address"]) then
        local cfip = ngx.req.get_headers()["CloudFront-Viewer-Address"]
        n = string.find(cfip, ":")
        ip = string.sub(cfip, 1, n-1)
        return ip
      else
        return ngx.var.remote_addr
      end
    

その後は同じ様にfastcgi等に $real_ipを渡します。

余談

今までnginxのdocker imageを使用していましたが、luaが使えなかったので今回はopenrestyのdocker imageを採用しました。