[Fixes #318] Attempt to improve README a bit (#323)

* docs: Improve Getting Started section

* docs: Following Getting Started show Usage to the README reader

* docs: Move the configuration tip to the Usage section

* docs: Move the cache store configuration comment to Usage

* docs: Clarify Responses title

* docs: allow2ban also uses the cache store

* docs: Improve Usage docs for blocking, safelisting and throttling

* docs: Don't give the impression that the gem is not being maintained when it actually is

* docs: Be a bit more clear about cache store in README

* docs: Attempt to be a bit more concise in the README intro

* docs: Clarify sentence
This commit is contained in:
Gonzalo Rodriguez 2018-03-29 11:42:27 -03:00 committed by GitHub
parent 644ca8ff55
commit 4c1aa8b312
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

242
README.md
View file

@ -1,10 +1,8 @@
# Rack::Attack!!! # Rack::Attack
*Rack middleware for blocking & throttling abusive requests* *Rack middleware for blocking & throttling abusive requests*
Rack::Attack is a rack middleware to protect your web app from bad clients. 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.
It allows *safelisting*, *blocklisting*, *throttling*, and *tracking* based on arbitrary properties of the request.
Throttle and fail2ban state is stored in a configurable cache (e.g. `Rails.cache`), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).
See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack. See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
@ -12,99 +10,97 @@ See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hac
[![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack) [![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
[![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](https://codeclimate.com/github/kickstarter/rack-attack) [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](https://codeclimate.com/github/kickstarter/rack-attack)
## Looking for maintainers
I'm looking for new maintainers to help me support Rack::Attack. Check out
[issue #219 for details](https://github.com/kickstarter/rack-attack/issues/219).
## Getting started ## Getting started
Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to your Gemfile with bundler: ### 1. Installing
Add this line to your application's Gemfile:
```ruby ```ruby
# In your Gemfile # In your Gemfile
gem 'rack-attack' gem 'rack-attack'
``` ```
Tell your app to use the Rack::Attack middleware.
For Rails apps: And then execute:
$ bundle
Or install it yourself as:
$ gem install rack-attack
### 2. Plugging into the application
Then tell your ruby web application to use rack-attack as a middleware.
a) For __rails__ applications:
```ruby ```ruby
# In config/application.rb # In config/application.rb
config.middleware.use Rack::Attack config.middleware.use Rack::Attack
``` ```
Or for Rackup files: b) For __rack__ applications:
```ruby ```ruby
# In config.ru # In config.ru
require "rack/attack" require "rack/attack"
use Rack::Attack use Rack::Attack
``` ```
Add a `rack_attack.rb` file to `config/initializers/`: __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.
```ruby
# In config/initializers/rack_attack.rb ## Usage
class Rack::Attack
# your custom configuration...
end
```
*Tip:* The example in the wiki is a great way to get started: *Tip:* The example in the wiki is a great way to get started:
[Example Configuration](https://github.com/kickstarter/rack-attack/wiki/Example-Configuration) [Example Configuration](https://github.com/kickstarter/rack-attack/wiki/Example-Configuration)
Optionally configure the cache store for throttling or fail2ban filtering: 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.
#### `safelist_ip(ip_address_string)`
E.g.
```ruby ```ruby
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache # config/initializers/rack_attack.rb (for rails app)
Rack::Attack.safelist_ip("5.6.7.8")
``` ```
Note that `Rack::Attack.cache` is only used for throttling and fail2ban filtering; not blocklisting & safelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html). #### `safelist_ip(ip_subnet_string)`
## How it works E.g.
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](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
```ruby ```ruby
def call(env) # config/initializers/rack_attack.rb (for rails app)
req = Rack::Attack::Request.new(env)
if safelisted?(req) Rack::Attack.safelist_ip("5.6.7.0/24")
@app.call(env)
elsif blocklisted?(req)
self.class.blocklisted_response.call(env)
elsif throttled?(req)
self.class.throttled_response.call(env)
else
tracked?(req)
@app.call(env)
end
end
``` ```
Note: `Rack::Attack::Request` is just a subclass of `Rack::Request` so that you #### `safelist(name, &block)`
can cleanly monkey patch helper methods onto the
[request object](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack/request.rb).
## About Tracks Name your custom safelist and make your ruby-block argument return a truthy value if you want the request to be blocked, and falsy otherwise.
`Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes. The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
## Usage E.g.
Define safelists, blocklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise. In a Rails app
these go in an initializer in `config/initializers/`.
A [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request) object is passed to the block (named 'req' in the examples).
### Safelists
```ruby ```ruby
# config/initializers/rack_attack.rb (for rails apps)
# Provided that trusted users use an HTTP request header named APIKey
Rack::Attack.safelist("mark any authenticated access safe") do |request|
# Requests are allowed if the return value is truthy
request.env["APIKey"] == "secret-string"
end
# Always allow requests from localhost # Always allow requests from localhost
# (blocklist & throttles are skipped) # (blocklist & throttles are skipped)
Rack::Attack.safelist('allow from localhost') do |req| Rack::Attack.safelist('allow from localhost') do |req|
@ -113,16 +109,44 @@ Rack::Attack.safelist('allow from localhost') do |req|
end end
``` ```
### Blocklists ### Blocking
#### `blocklist_ip(ip_address_string)`
E.g.
```ruby ```ruby
# Block requests from 1.2.3.4 # config/initializers/rack_attack.rb (for rails apps)
Rack::Attack.blocklist('block 1.2.3.4') do |req|
Rack::Attack.blocklist_ip("1.2.3.4")
```
#### `blocklist_ip(ip_subnet_string)`
E.g.
```ruby
# config/initializers/rack_attack.rb (for rails apps)
Rack::Attack.blocklist_ip("1.2.0.0/16")
```
#### `blocklist(name, &block)`
Name your custom blocklist and make your ruby-block argument returna a truthy value if you want the request to be blocked, and falsy otherwise.
The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
E.g.
```ruby
# config/initializers/rack_attack.rb (for rails apps)
Rack::Attack.blocklist("block all access to admin") do |request|
# Requests are blocked if the return value is truthy # Requests are blocked if the return value is truthy
'1.2.3.4' == req.ip request.path.start_with?("/admin")
end end
# Block logins from a bad user agent
Rack::Attack.blocklist('block bad UA logins') do |req| Rack::Attack.blocklist('block bad UA logins') do |req|
req.path == '/login' && req.post? && req.user_agent == 'BadUA' req.path == '/login' && req.post? && req.user_agent == 'BadUA'
end end
@ -135,6 +159,8 @@ This pattern is inspired by [fail2ban](http://www.fail2ban.org/wiki/index.php/Ma
See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on See the [fail2ban documentation](http://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. 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](#cache-store-configuration) (which defaults to `Rails.cache` if present).
```ruby ```ruby
# Block suspicious requests for '/etc/password' or wordpress specific paths. # Block suspicious requests for '/etc/password' or wordpress specific paths.
# After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes. # After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
@ -155,8 +181,12 @@ end
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}"`. 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
`Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving `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. 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](#cache-store-configuration) (which defaults to `Rails.cache` if present).
```ruby ```ruby
# Lockout IP addresses that are hammering your login page. # Lockout IP addresses that are hammering your login page.
# After 20 requests in 1 minute, block all requests from that IP for 1 hour. # After 20 requests in 1 minute, block all requests from that IP for 1 hour.
@ -171,33 +201,40 @@ Rack::Attack.blocklist('allow2ban login scrapers') do |req|
end end
``` ```
### Throttling
### Throttles Throttle state is stored in a [configurable cache](#cache-store-configuration) (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](http://www.rubydoc.info/gems/rack/Rack/Request).
E.g.
```ruby ```ruby
# Throttle requests to 5 requests per second per ip # config/initializers/rack_attack.rb (for rails apps)
Rack::Attack.throttle('req/ip', limit: 5, period: 1.second) do |req|
# If the return value is truthy, the cache key for the return value
# is incremented and compared with the limit. In this case:
# "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
#
# If falsy, the cache key is neither incremented nor checked.
req.ip Rack::Attack.throttle("requests by ip", limit: 5, period: 2) do |request|
request.ip
end end
# Throttle login attempts for a given email parameter to 6 reqs/minute # Throttle login attempts for a given email parameter to 6 reqs/minute
# Return the email as a discriminator on POST /login requests # Return the email as a discriminator on POST /login requests
Rack::Attack.throttle('logins/email', limit: 6, period: 60) do |req| Rack::Attack.throttle('limit logins per email', limit: 6, period: 60) do |req|
req.params['email'] if req.path == '/login' && req.post? if req.path == '/login' && req.post?
req.params['email']
end
end end
# You can also set a limit and period using a proc. For instance, after # You can also set a limit and period using a proc. For instance, after
# Rack::Auth::Basic has authenticated the user: # Rack::Auth::Basic has authenticated the user:
limit_proc = proc { |req| req.env["REMOTE_USER"] == "admin" ? 100 : 1 } limit_proc = proc { |req| req.env["REMOTE_USER"] == "admin" ? 100 : 1 }
period_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 1.second : 1.minute} period_proc = proc { |req| req.env["REMOTE_USER"] == "admin" ? 1 : 60 }
Rack::Attack.throttle('req/ip', limit: limit_proc, period: period_proc) do |req|
req.ip Rack::Attack.throttle('request per ip', limit: limit_proc, period: period_proc) do |request|
request.ip
end end
``` ```
@ -223,7 +260,17 @@ ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, r
end end
``` ```
## Responses ### 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](https://rubygems.org/gems/redis)).
```ruby
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](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
## Customizing responses
Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html). Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html).
@ -288,6 +335,43 @@ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, r
end end
``` ```
## 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](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
```ruby
def call(env)
req = Rack::Attack::Request.new(env)
if safelisted?(req)
@app.call(env)
elsif blocklisted?(req)
self.class.blocklisted_response.call(env)
elsif throttled?(req)
self.class.throttled_response.call(env)
else
tracked?(req)
@app.call(env)
end
end
```
Note: `Rack::Attack::Request` is just a subclass of `Rack::Request` so that you
can cleanly monkey patch helper methods onto the
[request object](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack/request.rb).
### About Tracks
`Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
## Testing ## Testing
A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will