RailsでRack::AttackによるIP制限を実装する

わけあってRailsでIP制限できないものかといろいろやってみました。もちろんアプリケーションレイヤーではなくアクセス的にはもっと前のネットワークとかWebサーバーとかでやるほうが良いんですが、いろんな理由でそれができない場合、Railsでやってみたという話。

概要

  • いろいろあってアプリケーションレイヤーでIP制限したくなった
  • Railsでやる方法としてrack-attackでやる方法を考えてみた
  • punditでやる方法も考えてみた
  • どっちかというとrack-attackのほうが良いと思うけど、punditでも悪くない。でもCloudFlareとかと相性悪い

なぜやったか

例えばHerokuのようなPaaSを使ってる場合、nginxなどのWebサーバーのレイヤーでなにかさせたくても手を入れれない部分があります。その代替としてFirewall側で制御するのも1つの手ですが、仕方なくアプリケーションレイヤーでやらなきゃいけない場面もあることでしょう。

もちろん仕方なくなので、できるのであればもっと前のレイヤーでやっておくにこしたことはないんですが、それはさておきRailsでやるとしたらどうできるかを考えてやってみました。

実際のケースとしては、たとえば管理者しかアクセスできないページを用意してパスワードなどの認証以外の手段、もしくは併用する形で制限したい場合など。

期待することと前提

アプリケーションレイヤーでやる利点としては、認可の部分に相当し、アクセス権があった場合、なかった場合のハンドリングも柔軟に行なえることです。

今回はRailsだけで特定のIP以外はアクセスできないページが作れたら良しとします。前準備としては

Rails.application.routes.draw do
  root "home#index"
  
  namespace :admin do # admin配下には特定のIPでないとアクセスできないようにしたい
    get "/", to: 'admin#index'
  end
end

が最小のrouting構成でしょうか。要は普通にアクセスできるところとは別に/admin以下は特定のIPのみアクセスが許されているようなケースですね。

実際やったこと

Gem Rack::Attackを使う

Rack::AttackというGemを使いました。

厳密にはRailsではなくRailsが使ってるmiddlewareのrack用のライブラリであるRack::Attackで制御するアプローチになります。

Gemfileに

gem 'rack-attack'

と書いてbundle installします。ここはいつもどおり。ドキュメントにあるようにデフォルトでRailsでは適用されます。

逆にdevelopmentやtest環境では制限しないなどの場合はGemfile側で

group :production do
  gem 'rack-attack'
end

とするか、もしくはconfig/environments/develpment.rbなどの環境ごとの設定で

Rack::Attack.enabled = false

を書いて適用されないように回避するのが良いでしょう。

次に設定ファイルconfig/initializers/rack_attack.rbを作ります。この中で

Rack::Attack.safelist('allow from localhost and specific IPs') do |req|
	return true unless req.path.start_with?('/admin')
	safe_ips = ["127.0.0.1", "123.456.7.8"]
	req.ip.in?(safe_ips)
end

みたいな感じでやってみました。

safelistはtrueを返す場合はアクセスを許可ですね。'/admin'以下であることの制御をsafelist内でやっていますが、もっと良いアプローチもありそうな気もします。

今回は特定のIPのみを通したいのでsafelistですが、特定の条件のみを弾くblocklistも書けますので、条件式を工夫すれば同じことはblocklistでも実現できますね。書きやすいほうで。

その他ではアクセス頻度による制御も可能です。このあたりの詳しいことは公式ドキュメントに詳しくわかりやすく書いてありますのでご参照ください。

やってみた結果

今回はRack::Attackを用いてRack層でアプローチしてみました。Fail2Ban相当のこともできるようなので今回のアプリケーションレイヤーで制限したいという要望にはマッチしていますね。

実際手持ちの環境で試してみた結果としては上手くいきませんでした。というのも間にCDN(CloudFlare)を噛ましていたためIPがそこから先は変わってしまうのが問題でした。これはRack::Attackが悪いのではなく、そもそもアプリケーション側でIP制限をしようというアプローチがCDNと相性が悪いという話です。

それ以外では特に大きくハマることもなく実装できました。強いていえば出回ってる紹介記事が古いものが多いようなので公式のReadMeを見るのが一番良いということぐらいでした。

今回紹介しなかったこと

公開紹介しなかったアプローチとして、認可用のGemであるPunditでも実現できました。簡単に言えばrequestはpunditでも取れるので、認可(Authorize)の中で特定のIPかどうかを判定すれば良いだけですね。
これは次回にでもまた別に紹介したいと思います。

RailsでIP制限をPunditで実装してみる来た道、行く道 LifeJourney[39.0/2020-10](月次振り返り)