こんにちは、hachi8833です。今回はrack-attack gemを取り上げます。
🔗 rack-attack gem
2012年のv1.0リリース以来長く使われていて実績があるのが助かります(現在はv6.6.1)。GitHubの★も5,300個を超えていますね。
rack-attackを利用可能なRailsバージョンはAppraisalsファイルで確認できます。
- 以前はGitHubのkickstarter/rack-attackに置かれていましたが、現在はrack/rack-attackに置かれています。
-
Sinatraでは、以前は別gemだったsinatra/rack-protectionが本体に統合されました。
🔗 rack-attack gemとは
Railsが動作する土台となるRack middleware(後述)のレベルでアプリへのアクセスを制限できます。
アクセス制限をRailsアプリ側のコードで実装せずに済むので、開発がシンプルになります。逆にアクセス制限をRailsアプリと密に連携しようとすると大変かもしれません。
rack-attackの設定はRubyのコードになっているので、込み入った要件にも対応可能です(Rack::Attack
というクラスが使えます)。
クラウドやホスティング環境でも類似のアクセス制限は多くの場合可能ですし、より下のレイヤで制限をかける方がパフォーマンス上有利ですが、設定は別途記録が必要になります。
rack-attackの設定ならリポジトリに登録できるので、アクセス制限の履歴をRailsアプリのGitログなどで容易に追えるようになります。
以下の制限がチェックされます。
- safelist
- アクセスを許可する対象を記述する
- blocklist
- アクセスをブロックする対象を記述する
- throttle
- 許容するアクセス回数や頻度の上限を指定し、超えたらブロックする
- track
- その他のチェック(ログ出力など)を行う
また、利用するキャッシュストアも設定・カスタマイズ可能です。
上記はattack.rbで以下のような流れで処理されます。
def call(env)
return @app.call(env) if !self.class.enabled || env["rack.attack.called"]
env["rack.attack.called"] = true
env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
request = Rack::Attack::Request.new(env)
if configuration.safelisted?(request)
@app.call(env)
elsif configuration.blocklisted?(request)
# Deprecated: Keeping blocklisted_response for backwards compatibility
if configuration.blocklisted_response
configuration.blocklisted_response.call(env)
else
configuration.blocklisted_responder.call(request)
end
elsif configuration.throttled?(request)
# Deprecated: Keeping throttled_response for backwards compatibility
if configuration.throttled_response
configuration.throttled_response.call(env)
else
configuration.throttled_responder.call(request)
end
else
configuration.tracked?(request)
@app.call(env)
end
end
アクセス制限では、IPアドレスやURL、UserAgent(≒ブラウザの種類)、HTTPレスポンスなど、さまざまな要素を指定できます。ログ出力(RailsのActiveSupport::Notifications
を利用↓)にも対応しています。
参考: Active Support の Instrumentation 機能 - Railsガイド
🔗 rack-attack gemの使いみち
以下のような使い方が主になると思います。
- 短時間の大量アクセス(DOS攻撃)を遮断したい
- /loginページで同一IPアドレスからの連続POSTリクエスト(パスワードの総当り攻撃)を遮断したい
- 特定のIPアドレスにのみアクセスを許可したい
- 特定のURLだけ特定のIPアドレスにのみアクセスを許可したい
同一IPアドレスからの短時間/大量アクセスの制限は、特にインターネットで一般公開されるRailsアプリで役立ちそうです。非常に大規模な大量アクセスについては、rack-attackだけに任せず、他の方法によるアクセス制限も併用する必要があるかもしれません。
# examples/rack_attack.rbより
# 同一IPからの1秒あたりの最大アクセス数を10回に制限
Rack::Attack.throttle("req/ip", limit: 10, period: 1) { |req| req.ip }
請け負っているRailsアプリのセキュリティに問題が見つかった場合に、取り急ぎrack-attackでアクセスを絞り込んでからセキュリティ問題に対処するといった使い方も考えられます。
🔗 rack-attack gemの導入
READMEにひととおり書いてありますが、一応こちらにも書いておきます。
- Gemfileに以下を追記し、
bundle install
を実行します。またはgem install rack-attack
でインストールできます。
gem 'rack-attack'
rack-attackをRailsアプリケーションにインストールすると、デフォルトで有効になります。
# rack-attackを無効にしたい場合の設定
Rack::Attack.enabled = false
rackアプリケーションの場合は以下のように設定します。
# config.ru (Rackupファイル)に書く場合
require "rack/attack"
use Rack::Attack
重要: デフォルトのrack-attackは、何らかのルールを設定するまではアクセスのブロックやスロットリングを一切行いません。
🔗 設定方法
config/initializers/ディレクトリの下にrack_attack.rbを作成し、以下のような要領でルールを定義します。
# ブロックを使わない場合
Rack::Attack.safelist_ip("5.6.7.8")
# ブロックを使う場合
Rack::Attack.safelist("mark any authenticated access safe") do |request|
# 戻り値がtruthyの場合はリクエストを許可する
request.env["HTTP_APIKEY"] == "secret-string"
end
初めての場合は一般的な設定例を参考にして少しずつ調整するとよいでしょう。以下に抜粋しました。
# 例: 同一IPアドレスからのリクエスト数上限を5分あたり300回に制限
throttle('req/ip', limit: 300, period: 5.minutes) do |req|
req.ip # unless req.path.start_with?('/assets')
end
# 例: 同一IPアドレスからの/loginsへのPOSTリクエストを20秒あたり5回までに制限
throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
if req.path == '/login' && req.post?
req.ip
end
end
より高度な設定方法については高度な設定例をご覧ください。ブラックリストを環境変数で与える方法や、BASIC認証への攻撃を遮断する方法などが記載されています。「大半のコードはテストされてないのでご注意ください」とあります。
通常利用で支障が生じないようにするため、設定値を厳しくしすぎないように注意しましょう。設定に引っかかるとリクエストがRailsアプリに渡されなくなるので、該当ユーザーから見て応答なしの状態になります。
🔗 参考: RailsのRack middlewareについて
技術評論社の「Rackとは何か」が一番参考になると思います。Rack middlewareに関するその他の一次情報リンクは以下のとおりです。
更新履歴