[Awesome Ruby Gem] Use rack-attack gem to protect your Rails and Rack apps from bad clients
rack-attack
rack-attack is a Rack middleware for blocking & throttling abusive requests
Protect your Rails and Rack apps from bad clients. Rack::Attack lets you easily decide when to allow, block and throttle based on properties of the request.
Installation
You can install it as a gem:
1 | gem install rack-attack |
or add it into a Gemfile (Bundler):
1 | # Gemfile |
Then, run bundle install
.
1 | bundle install |
Configuration
Then tell your ruby web application to use rack-attack as a middleware.
- a) For rails applications it is used by default.
You can disable it permanently (like for specific environment) or temporarily (can be useful for specific test cases) by writing:
1 | Rack::Attack.enabled = false |
- b) For rack applications:
1 | # In config.ru |
IMPORTANT: By default, rack-attack won’t perform any blocking or throttling, until you specifically tell it what to protect against by configuring some rules.
Usage
Tip: If you just want to get going asap, then you can take our example configuration - https://github.com/rack/rack-attack/blob/master/docs/example_configuration.md and tailor it to your needs, or check out the advanced configuration - https://github.com/rack/rack-attack/blob/master/docs/advanced_configuration.md examples.
Define rules by calling Rack::Attack
public methods, in any file that runs when your application is being initialized. For rails applications this means creating a new file named config/initializers/rack_attack.rb
and writing your rules there.
Safelisting
Safelists have the most precedence, so any request matching a safelist would be allowed despite matching any number of blocklists or throttles.
1 | safelist_ip(ip_address_string) |
E.g.
1 | # config/initializers/rack_attack.rb (for rails app) |
safelist_ip(ip_subnet_string)
E.g.
1 | # config/initializers/rack_attack.rb (for rails app) |
safelist(name, &block)
Name your custom safelist and make your ruby-block argument return a truthy value if you want the request to be allowed, and falsy otherwise.
The request object is a Rack::Request
.
E.g.
1 | # config/initializers/rack_attack.rb (for rails apps) |
Blocking
blocklist_ip(ip_address_string)
E.g.
1 | # config/initializers/rack_attack.rb (for rails apps) |
blocklist_ip(ip_subnet_string)
E.g.
1 | # config/initializers/rack_attack.rb (for rails apps) |
blocklist(name, &block)
Name your custom blocklist and make your ruby-block argument return a truthy value if you want the request to be blocked, and falsy otherwise.
The request object is a Rack::Request
.
E.g.
1 | # config/initializers/rack_attack.rb (for rails apps) |
Fail2Ban
Fail2Ban.filter
can be used within a blocklist to block all requests from misbehaving clients. This pattern is inspired by fail2ban. See the fail2ban documentation - https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options for more details on how the parameters work. For multiple filters, be sure to put each filter in a separate blocklist and use a unique discriminator for each fail2ban filter.
Fail2ban state is stored in a configurable cache (which defaults to Rails.cache
if present).
1 | # Block suspicious requests for '/etc/password' or wordpress specific paths. |
Note that Fail2Ban filters are not automatically scoped to the blocklist, so when using multiple filters in an application the scoping must be added to the discriminator e.g. “pentest:#{req.ip}”.
Allow2Ban
Allow2Ban.filter
works the same way as the Fail2Ban.filter
except that it allows requests from misbehaving clients until such time as they reach maxretry at which they are cut off as per normal.
Allow2ban state is stored in a configurable cache (which defaults to Rails.cache
if present).
1 | # Lockout IP addresses that are hammering your login page. |
Throttling
Throttle state is stored in a configurable cache (which defaults to Rails.cache
if present).
throttle(name, options, &block)
Name your custom throttle, provide limit and period as options, and make your ruby-block argument return the discriminator. This discriminator is how you tell rack-attack whether you’re limiting per IP address, per user email or any other.
The request object is a Rack::Request
.
E.g.
1 | # config/initializers/rack_attack.rb (for rails apps) |
Tracks
1 | # Track requests from a special user agent. |
Cache store configuration
Throttle, allow2ban and fail2ban state is stored in a configurable cache (which defaults to Rails.cache if present), presumably backed by memcached or redis (at least gem v3.0.0).
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
Note that Rack::Attack.cache is only used for throttling, allow2ban and fail2ban filtering; not blocklisting and safelisting. Your cache store must implement increment and write like ActiveSupport::Cache::Store
.
Customizing responses
Customize the response of blocklisted and throttled requests using an object that adheres to the Rack app interface.
1 | Rack::Attack.blocklisted_callback = lambda do |request| |
RateLimit headers for well-behaved clients
While Rack::Attack’s primary focus is minimizing harm from abusive clients, it can also be used to return rate limit data that’s helpful for well-behaved clients.
If you want to return to user how many seconds to wait until they can start sending requests again, this can be done through enabling Retry-After header:
1 | Rack::Attack.throttled_response_retry_after_header = true |
For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
1 | request.env['rack.attack.throttle_data'][name] # => { discriminator: d, count: n, period: p, limit: l, epoch_time: t } |
Logging & Instrumentation
Rack::Attack
uses the ActiveSupport::Notifications
API if available.
You can subscribe to rack_attack events and log it, graph it, etc.
To get notified about specific type of events, subscribe to the event name followed by the rack_attack
namespace. E.g. for throttles use:
1 | ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |name, start, finish, request_id, payload| |
If you want to subscribe to every rack_attack event, use:
1 | ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, request_id, payload| |
Testing
A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will need to enable the cache in your development environment. See Caching with Rails for more on how to do this.
Disabling
1 | Rack::Attack.enabled = false can be used to either completely disable Rack::Attack in your tests, or to disable/enable for specific test cases only. |
Test case isolation
Rack::Attack.reset! can be used in your test suite to clear any Rack::Attack state between different test cases.
How it works
The Rack::Attack middleware compares each request against safelists, blocklists, throttles, and tracks that you define. There are none by default.
If the request matches any safelist, it is allowed.
Otherwise, if the request matches any blocklist, it is blocked.
Otherwise, if the request matches any throttle, a counter is incremented in the Rack::Attack.cache. If any throttle’s limit is exceeded, the request is blocked.
Otherwise, all tracks are checked, and the request is allowed.
The algorithm is actually more concise in code: See Rack::Attack.call:
1 | def call(env) |
Note: Rack::Attack::Request is just a subclass of Rack::Request so that you can cleanly monkey patch helper methods onto the request object.
About Tracks
Rack::Attack.track doesn’t affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
References
[2] rack-attack | RubyGems.org | your community gem host - https://rubygems.org/gems/rack-attack/