Rails の trusted proxy めんどいところとワークアラウンド

食べログ DevOps チームの @weakboson です。
この記事は Advent Calendar の7日目の投稿です。

# X-Forwarded-For とは？

Production 構成では Web アプリケーションが直接クライアントのリクエストを受けることはあまりないでしょう。Ruby on Rails もクライアントとの間に HTTP サーバを reverse proxy として配置して、レスポンスの gzip 圧縮、SSL 終端、静的なファイル配信は HTTP サーバに任せることが推奨されています。更に上流にはリクエストを複数の reverse proxy に分配する専用のロードバランサーが控えていることが多いでしょう。

このような proxy 構成では TCP/IP パケットの IP ヘッダにある送信元アドレスは一つ上流のサーバの IP アドレスになっており (※) アプリケーションは TCP/IP のレイヤー (L4) ではリモートホストの IP アドレスがわかりません。しかし、業務要件でリモートホストの IP アドレスが必要なことはよくあります。そこで HTTP ヘッダでリモートホストの IP アドレスを下流にリレーして伝える方式が考案され Web アプリの慣例的標準になりました。この HTTP ヘッダが `X-Forwarded-For` です。X-Forwarded-For は複数の値をカンマ区切りで並べることができ、途中の proxy をすべて記録して下流に伝えることができます。複数の値があるときは一番左がクライアント、そこから通過してきた proxy が順番に右に追加されてゆきます。

※ これは NAT をするロードバランスの特徴です。 Direct Server Return (DSR) というレスポンスはロードバランサを介さずサーバが直接クライアントに返す方式もあり、DSR では IP ヘッダの送信元アドレスはクライアントの IP アドレスのままになっています。

# 信頼できる proxy (trusted proxies) とは何か？

X-Forwarded-For は単なる HTTP ヘッダなので詐称が容易です。クライアント自身が偽って付加してきたときは適切に排除する必要があります。下流に意図しない加工をしたヘッダを送りつけてこないと確証できる proxy からのリクエストに付加されているヘッダに限り信頼すべきでしょう。

Rails には `trusted_proxies` というホワイトリストがあり、そこからのリクエストに付与されている X-Forwarded-For は信頼できる値としてリモートホストの IP アドレス (`ActionDispatch::Request#remote_ip`) として扱われます。

trusted_proxies のデフォルト値は Private Address A, B, C とローカルアドレスです。

[rails/actionpack/lib/action_dispatch/middleware/remote_ip.rb の The default trusted IPs list](https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L35-L42)

そこまではいいんですが trusted_proxies に含まれるホスト自身がクライアントとしてリクエストを投げても「proxy だ！！proxy だろう！？なあ proxy だろうおまえ」という感じでクライアント扱いしてくれないんですよ。`request.remote_ip` としても直接上流のホスト IP アドレスが取れてきます。

この仕様が困るのはどういうときか？

- クライアントが Private Address であるオンプレミスの社内システム。機能によってはきちんと audit ログを取りたいし、エラーが起きたら「いまどういう操作したの？」と社内の人に尋ねたい
- 同じくオンプレミス環境で社内監視機能からのアクセスを特別扱いしたいとき

2つ目は「なにそれ？」って感じなのですが、食べログは定期的にサービスの url 叩いて規定時間内にレスポンスが返ってくるか監視しています。アクセス元個々のリモート IP アドレスが知りたいわけではないのだけれど、監視機器とそれ以外のアクセスは区別したいということがありました。

# Private Address のリモート IP をきちんと取りたい ~ ガチ対応編

```ruby:config/environments/production.rb
require 'ipaddr'
Rails.application.configure do
  # 中略
  # 10.255.0.1 ほか7台が reverse proxy
  # 残りは ipv4, ipv6 での localhost
  config.action_dispatch.trusted_proxies = %w{
    10.255.0.1/29
    127.0.0.1
    ::11
}.map{|proxy| IPAddr.new(proxy) }
```

Private Address 全部というデフォルトの大雑把な設定にしないで実際に reverse_proxy として運用しているホストだけを設定します。

かんたんですが長期運用で reverse proxy の増減があることを想定すると少しいやですね。信頼できるネットワーク中にあってアプリケーションが上流にある reverse proxy を把握しておく必要はないのでは？という気がします。

# Private Address が大体どういうヤツかわかればいいや ~ ずる？編

Nginx さん！Apache さん！~~こんなに綺麗な花火ですよ~~ 出番ですよ！

Reverse proxy の上流も信頼できるロードバランサである場合、HTTP サーバでクライアントを分類する手があります。「社内の監視機器を区別したいだけ」というユースケースであればアプリケーションが監視機器の IP を知らなくてもよくなり一石二鳥感があります。

Nginx では [geo module](http://nginx.org/en/docs/http/ngx_http_geo_module.html) がこんなとき便利です。

```nginx:nginx.conf
http {
    geo $client_type {
        10.255.241.1/29 monitor;
        10.0.0.0/8      intra;
        172.16.0.0/12   intra;
        192.168.0.0/16  intra;
        default         customer;
    }

    server {
        server_name example.com;
        proxy_set_header X-Client-Type $client_type;
        location / {
            proxy_pass http://application;
        }
    }
}
```

```ruby:railsのコード
  if request.headers['X-Client-Type'] == 'monitor'
    ...
```

実際に使うときは `ActionDispatch::Request` を拡張して `#monitor?` のようなメソッドを生やしてやるのがよいでしょうね。

# 締め

改めてまとめるとオンプレミスのサーバを保有している組織限定の悩みで、クラウドネイティブで Private Address のサーバがない環境ではこういうこと気にもしないのかなーなどと思いました。

明日、8日目も私による「Crostini LXC コンテナバックアップとリストア ~ 不安定な Dev Channel でもくじけない」です。

# オマケ Rails 5.0 でログに remote_ip をタグ付けしても ip になっちゃう件

Rails 5.0 では RemoteIp より前に Logger を Rack に積んでいるせいで、

```ruby
config.log_tags = [:uuid, :remote_ip]
```

のように設定してもログには `Request#ip` (proxy の IP アドレス) が記録されてしまいます。Rails 5.1 から以下の修正が取り込まれ、正しく `Request#remote_ip` が記録できます。

[Allow log remote ip addres when config.action_dispatch.trusted_proxie… by kbrock · Pull Request #27515 · rails/rails](https://github.com/rails/rails/pull/27515)