Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-ha
- [Cache store configuration](#cache-store-configuration)
- [Customizing responses](#customizing-responses)
- [RateLimit headers for well-behaved clients](#ratelimit-headers-for-well-behaved-clients)
- [Blocking Based on Response](#blocking-based-on-response)
- [Logging & Instrumentation](#logging--instrumentation)
- [Testing](#testing)
- [How it works](#how-it-works)
Expand Down Expand Up @@ -373,6 +374,14 @@ For responses that did not exceed a throttle limit, Rack::Attack annotates the e
request.env['rack.attack.throttle_data'][name] # => { discriminator: d, count: n, period: p, limit: l, epoch_time: t }
```

## Blocking Based on Response

Sometimes you want to block requests based on the response rather than just the request properties. For example, you might want to block IPs that receive multiple 404 responses (potential pentesters scanning for vulnerabilities) or multiple 401 responses (brute force login attempts).

Since `Rack::Attack` runs before your application processes the request, it cannot directly access response status codes. However, you can achieve this using a custom middleware that runs *after* your application, combined with `Fail2Ban` filters.

For a complete working example with middleware implementation and setup instructions, see the [Block Based on Response](docs/advanced_configuration.md#block-based-on-response) section in the advanced configuration guide.

## Logging & Instrumentation

Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API if available.
Expand Down
112 changes: 112 additions & 0 deletions docs/advanced_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,118 @@ Rack::Attack.blocklist('basic auth crackers') do |req|
end
```

### Block Based on Response

Sometimes you want to block based on the HTTP response, rather than just the request properties. Common use cases include:

- **Blocking pentesters**: IPs that receive multiple 404 responses are likely scanning for vulnerabilities
- **Blocking brute force attacks**: IPs that receive multiple 401 responses are likely attempting to guess credentials
- **Blocking scrapers**: IPs that trigger many error responses may be poorly configured bots

Since `Rack::Attack` runs as middleware *before* your application processes requests, it cannot directly access response properties. The solution is to create a custom middleware that runs *after* your application and uses `Fail2Ban` to track response properties.

#### Example implementation

First, create a middleware that tracks response status codes. This middleware should be placed in your application (e.g., `app/middleware/response_tracker_middleware.rb` in Rails):

```ruby
# app/middleware/response_tracker_middleware.rb
class ResponseTrackerMiddleware
# Define filters that will be shared between this middleware and Rack::Attack
FILTERS = [
->(request, condition = nil) do
# Track IPs that receive 404 responses
# After 50 404s in 10 seconds, ban the IP for 60 seconds
Rack::Attack::Fail2Ban.filter("pentesters-#{request.ip}", maxretry: 50, findtime: 10, bantime: 60) { condition }
end
]

def initialize(app)
@app = app
end

def call(env)
# Let the application process the request first
status, headers, body = @app.call(env)

# Check the response status and update Fail2Ban counters
request = Rack::Attack::Request.new(env)
FILTERS.each do |filter|
# Increment the counter if status is 404
filter.call(request, status == 404)
end

[status, headers, body]
end
end
```

Next, configure `Rack::Attack` to use the same filter for blocking. Add this to your Rack::Attack initializer (e.g., `config/initializers/rack_attack.rb`):

```ruby
# config/initializers/rack_attack.rb
# Use the same filters defined in ResponseTrackerMiddleware
# This checks if an IP should be blocked, but does NOT increment the counter
ResponseTrackerMiddleware::FILTERS.each do |filter|
Rack::Attack.blocklist('pentesters by 404') do |request|
filter.call(request)
end
end
```

Finally, add the middleware to your application stack.

```ruby
# config/application.rb (for Rails)
config.middleware.use ResponseTrackerMiddleware
```

Or for a Rack application:

```ruby
# config.ru
use Rack::Attack
use ResponseTrackerMiddleware
run YourApp
```

#### How It Works

1. A request comes in and passes through `Rack::Attack` first
2. `Rack::Attack` checks the blocklist, which calls the filter *without* incrementing the counter
3. If the IP is already banned (from previous 404s), the request is blocked with a 403 response
4. If not blocked, the request continues to your application
5. Your application processes the request and returns a response (e.g., 404)
6. `ResponseTrackerMiddleware` runs after your app and checks the status code
7. If status is 404, it increments the Fail2Ban counter for that IP
8. Once the IP exceeds `maxretry` 404s within `findtime`, subsequent requests are blocked

#### Customization

You can adapt this pattern for other use cases:

```ruby
# Block IPs with multiple 401 responses (failed login attempts)
->(request, condition = nil) do
Rack::Attack::Fail2Ban.filter("login-failures-#{request.ip}", maxretry: 5, findtime: 1.minute, bantime: 10.minutes) { condition }
end

# Then in the middleware:
filter.call(request, status == 401 && request.path == '/login')
```

```ruby
# Block IPs with any 4xx error
->(request, condition = nil) do
Rack::Attack::Fail2Ban.filter("client-errors-#{request.ip}", maxretry: 10, findtime: 5.minutes, bantime: 1.hour) { condition }
end

# Then in the middleware:
filter.call(request, status >= 400 && status < 500)
```

**Note**: The same filter instance must be used in both the middleware (to increment counters) and the Rack::Attack blocklist (to check if IP should be blocked).

### Match Actions in Rails

Instead of matching the URL with complex regex, it can be much easier to match specific controller actions:
Expand Down