こんにちは。サーバーサイドエンジニアの三村(@t_mimura39)です。
本日はRuby・Railsの話に限定せず、Amazon CloudFront を利用している方に役立つ情報をご提供します。
目次
はじめに
弊社は基本的にAWS上にRailsアプリケーションを構築しているため、CDNが必要になるとまず選択肢として挙がるのが 「Amazon CloudFront(以下CloudFront)」です。
「CloudFront」を最前列に配置して、その後ろに「ALB」と「Railsアプリケーションが稼働するECS」を置くような構成が主流です。
この構成の場合一つ困ることがあります。 それは「RailsアプリケーションからクライアントのIPアドレスを取得できない」という点です。
Railsアプリケーションへのリクエストの途中にプロキシやロードバランサーが挟まるとそれらのIPアドレスを「クライアントのIPアドレス」とRailsが誤認してしまうためです。
この話はCloudFrontやRailsに限定されたものではなく、古くからWeb業界で認識されている内容かと思います。
「古くからWeb業界で認識されている」ということは回避方法があります。
まずは従来の回避方法について簡単にご紹介します。
「X-Forwarded-For」を活用する方法
「X-Forwarded-For(以下XFF)」とはクライアントIP アドレスを特定するために利用されるHTTPヘッダーです。
以下のような構文で経路上のIPアドレスがすべて含まれます。
X-Forwarded-For: <client>, <proxy1>, <proxy2>
基本的にはXFFの先頭のIPアドレスが「クライアントのIPアドレス」となるわけですが、本ヘッダーは容易に改ざん可能(≒ IPスプーフィングリスクがある)なため一工夫必要です。
本件については以下のブログ記事が詳しいため詳細はこちらをご参照ください。
mrk21.hatenablog.com www.m3tech.blog
誤解を恐れずに簡単に説明すると「CloudFrontを含む信頼できるIPアドレスの一覧を取得しXFFからそれらを間引いた結果の末尾のIPアドレスをクライアントのIPアドレスとみなす」といった仕組みになります。
この仕組みの実装例として以下のようなGemがあります。
もちろんこれらのGemを使わずとも自前でCloudFrontのIPアドレスを取得・管理し、nginxなどでヘッダーを調整するようなアプローチもあるかと思います。
実装方法はともかく、この仕組みでクライアントの安全にIPアドレスを取得することが可能になります。
古くから伝わっている手法のためセキュリティ的なリスクは低いですが、以下のような問題点があります。
- 背景も含め若干複雑な仕組みとなってしまう
- (実装方法によっては)「CloudFrontのIPアドレスが更新された際に即時反映されず正しくクライアントのIPアドレスが取得できない」のリスクがある
「CloudFront-Viewer-Address」を活用する方法
上記XFFの問題を解決するための手法の一つとして、「CloudFront-Viewer-Address」をご紹介します。
「CloudFront-Viewer-Address」は2021年頃提供された機能で特別新しい情報ではないのですが、弊社内ではあまり認識されていなかったため改めて今回取り上げてみました。
詳細は以下のブログ記事がとても詳しいので是非ご参照ください。CloudFrontの設定方法についても詳細に書かれていたためとても参考になりました。
簡単に説明しますとCloudFrontを経由するアクセスに対して、以下のような構文でCloudFrontのViewer(クライアント)のIPアドレスとポート番号をヘッダーに付け加えてくれる機能です。
CloudFront-Viewer-Address: <IPアドレス>:<ポート> # IPv4 IPアドレス=192.0.2.0, ポート=46532 CloudFront-Viewer-Address: 192.0.2.0:46532 # IPv6 IPアドレス=2001:DB8:0:0:8:800:200C:417A, ポート=46532 CloudFront-Viewer-Address: 2001:DB8:0:0:8:800:200C:417A:46532
本ヘッダーを活用することで、従来の手法(上記XFFの手法)よりもシンプルで確実にクライアントのIPアドレスを取得することができます。 一点注意点として、「CloudFront-Viewer-Address」はIPアドレスだけでなくポート番号が末尾に付与されるためここは微調整が必要です。
Railsエンジニアへ
ここからはRailsに限定されたお話です。
上記の通り「CloudFront-Viewer-Address」ヘッダーを利用することでクライアントのIPアドレスが取得できるのですが、いざRailsでリクエストヘッダーを参照するとなると少し面倒です。
正確にいうと、「単純にリクエストヘッダーを参照するだけ」であればこのように簡単に実装できます。
class ApplicationController def 本当のリモートIP if headers[:HTTP_CLOUDFRONT_VIEWER_ADDRESS].present? headers[:HTTP_CLOUDFRONT_VIEWER_ADDRESS].remove(/:\d+\z/) end end end
しかし、従来の request.remote_ip
のI/Fを維持するにはRack層で処理する必要がありそうです。
(モンキーパッチを当てたりすればどうとにでもなりますが、ここは健全なアプローチで話を進めます。)
というわけで、あらかじめ用意しておいたものがこちらになります。 github.com
本Gemをインストールするだけで、自動的に「CloudFront-Viewer-Address」を参照し request.remote_ip
でクライアントのIPアドレスが取得できるようになります。
専用のRackミドルウェア( ActionPack::CloudfrontViewerAddress::RemoteIp
)を作成し、自動的に ActionDispatch::RemoteIp
の後ろにinsertするような作りとなっています。
注意点
本Gemは「CloudFront-Viewer-Address」の値を信頼して実装しています。
Railsに対しての全てのリクエストがCloudFrontを経由していれば本ヘッダーは改ざんされることがないため安全です。
しかし、CloudFrontを経由しないアクセスを受け付けている場合は、任意のIPアドレスを「CloudFront-Viewer-Address」に指定することができてしまうためIPスプーフィングのリスクがあります。
各々のインフラ構成やIPアドレスの利用方法に応じて、ご利用の判断をしてください。
おまけ
CloudFrontには「CloudFront-Viewer-Address」の他にも便利なリクエストヘッダーを付与する機能があります。
例えばIPアドレスを元に「緯度・経度」「国名・都市名」なんかをCloudFrontが算出し、「CloudFront-Viewer-Address」と同じような感じでヘッダーに付与してくれるようなものです。
詳細はAWSの公式ドキュメントをご参照ください。
docs.aws.amazon.com
自前でGeo情報に問い合わせていたような処理を置き換えられることが期待できますね。
参考資料
- Amazon CloudFront はクライアント IP アドレスと接続ポートヘッダーのサポートを追加
- Amazon CloudFrontでクライアントのIPアドレスと接続ポートを確認できるCloudFront-Viewer-Addressヘッダが利用可能になりました | DevelopersIO
- X-Forwarded-For - HTTP | MDN
- X-Forwarded-For の正しい取り扱い方とCloudFrontを通したときのクライアントIPの取得方法 - mrk21::blog {}
- こんばんは、X-Forwarded-For警察です - エムスリーテックブログ
是非読者になってください!
メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!
■募集ポジションはこちら medpeer.co.jp
■エンジニア紹介ページはこちら engineer.medpeer.co.jp
■メドピア公式YouTube www.youtube.com
■メドピア公式note
style.medpeer.co.jp