A Rack middleware for throttling and filtering requests.
Rack::Defense is a Rack middleware that allows to easily add request rate limiting and request filtering to your Rack based application (Ruby On Rails, Sinatra etc.).
-
Request throttling (aka rate limiting) happens on sliding window using the provided period, request criteria and maximum request number. It uses Redis to track the request rate.
-
Request filtering bans (rejects) requests based on provided criteria.
Rack::Defense has a small footprint and only two dependencies: rack and redis.
Rack::Defense is inspired from the Rack::Attack project. The main difference is the throttling algorithm: Rack::Attack uses a counter reset at the end of each period, therefore allowing up to 2 times more requests than the maximum rate specified. We use a sliding window algorithm allowing a precise request rate limiting.
Install the rack-defense gem; or add it to you Gemfile with bundler:
# In your Gemfile
gem 'rack-defense'
Tell your app to use the Rack::Defense middleware. For Rails 3+ apps:
# In config/application.rb
config.middleware.use Rack::Defense
Or for Rackup files:
# In config.ru
use Rack::Defense
Add a rack-defense.rb
file to config/initializers/
:
# In config/initializers/rack-defense.rb
Rack::Defense.setup do |config|
# your configuration here
end
The Rack::Defense middleware evaluates the throttling criteria (lambdas) against the incoming request. If the return value is falsy, the request is not throttled. Otherwise, the returned value is used as a key to throttle the request. The returned key could be the request IP, user name, API token or any discriminator to throttle the requests against.
Throttle POST requests for path /login
with a maximum rate of 3 request per minute per IP:
Rack::Defense.setup do |config|
config.throttle('login', 3, 60 * 1000) do |req|
req.ip if req.path == '/login' && req.post?
end
end
Throttle GET requests for path /api/*
with a maximum rate of 50 request per second per API token:
Rack::Defense.setup do |config|
config.throttle('api', 50, 1000) do |req|
req.env['HTTP_AUTHORIZATION'] if %r{^/api/} =~ req.path
end
end
Throttle POST requests for path /aggregate/report
with a maximum rate of 10 requests per hour for a given logged in user. We assume here that we are using the Warden middleware for authentication or any Warden based authentication wrapper, like Devise in Rails.
Rack::Defense.setup do |config|
config.throttle('aggregate_report', 10, 1.hour.in_milliseconds) do |req|
req.env['warden'].user.id if req.path == '/aggregate/report' && req.env['warden'].user
end
end
Rack::Defense uses Redis to track request rates. By default, the REDIS_URL
environment variable is used to setup
the store. If not set, it falls back to host 127.0.0.1
port 6379
.
The redis store can be setup with either a connection url:
Rack::Defense.setup do |config|
config.store = "redis://:p4ssw0rd@10.0.1.1:6380/15"
end
or directly with a connection object:
Rack::Defense.setup do |config|
config.store = Redis.new(host: "10.0.1.1", port: 6380, db: 15)
end
Rack::Defense can reject requests based on arbitrary properties of the request. Matching requests are filtered out.
Allow only a whitelist of IPs for a given path:
Rack::Defense.setup do |config|
config.ban('ip_whitelist') do |req|
req.path == '/protected' && !['192.168.0.1', '127.0.0.1'].include?(req.ip)
end
end
Deny access to a blacklist of application users. Again, we assume here that Warden or any Warden based authentication wrapper, like Devise, is used:
Rack::Defense.setup do |config|
config.ban('user_blacklist') do |req|
['hacker@example.com', 'badguy@example.com'].include? req.env['warden'].user.email
end
end
Allow only requests with a known API authorization token:
Rack::Defense.setup do |config|
config.ban('validate_api_token') do |req|
%r{^/api/} =~ req.path && Redis.current.sismember('apitokens', req.env['HTTP_AUTHORIZATION'])
end
end
The previous example uses redis to keep track of valid api tokens, but any store (database, key-value store etc.) would do here.
By default, Rack::Defense returns 429 Too Many Requests
and 403 Forbidden
respectively for throttled and banned requests.
These responses can be fully configured in the setup:
Rack::Defense.setup do |config|
config.banned_response =
->(env) { [404, {'Content-Type' => 'text/plain'}, ["Not Found\n"]] }
config.throttled_response =
->(env) { [503, {'Content-Type' => 'text/plain'}, ["Service Unavailable\n"]] }
end
You can be notified when requests are throttled or banned. The callback receives the throttled request object and data about the event context.
For banned request callbacks, the triggered rule name is passed:
Rack::Defense.setup do |config|
config.after_ban do |req, rule|
logger.info "[Banned] #{rule} #{req.path} #{req.ip}"
end
end
For throttled request callbacks, a hash having triggered rule names as keys and the corresponding throttle keys as values is passed.
Rack::Defense.setup do |config|
config.after_throttle do |req, rules|
logger.info rules.map { |e| "[Throttled] rule name: #{e[0]} - rule throttle key: #{e[1]}" }.join ', '
end
end
In this example, when an IP is exceeding the permitted request rate, we would like to ban this IP for a given period of time:
Rack::Defense.setup do |config|
config.throttle('reset_password', 10, 10.minutes.in_milliseconds) do |req|
req.ip if req.path == '/api/users/password' && req.post?
end
config.after_throttle do |req, rules|
config.store.setex("ban:ip:#{req.ip}", 1.hour, 1) if rules.key? 'reset_password'
end
config.ban('blacklist') do |req|
config.store.exists("ban:ip:#{req.ip}")
end
end
The first rule named reset_password
defines the maximum permitted rate per IP for post requests on path
/api/users/password
. Once a user exceeds this limit, it gets throttled and denied access to the resource.
This raises a throttle event and triggers the after_throttle
callback defined above. The callback sets a key in the redis store post-fixed with the user IP and having 1 hour an expiration time.
The last rule named blacklist
looks up each incoming request IP and checks if it has a corresponding ban key
in redis. If the request IP matches a ban key it gets denied.
Licensed under the MIT License.
Copyright Chaker Nakhli.