Photo by Donald Giannatti on Unsplash

Protect your servers with Rack Attack.

Jorge Rodriguez
Get on Board Dev
Published in
3 min readJun 28, 2023

--

Your Rails application will be attacked, that is a fact, either today or in the future, it doesn’t matter whether your site is or not popular, attackers do it for money, for ego, or for fun.

Rack attack gem is a gem (pun intended) when it is about stopping attackers or adding limits to the rate of requests to your Rails application.

We have been iterating over setting up the gem to protect our servers not only from malicious people but also from abusive (in a good sense) users that hit our endpoints too frequently.

The final result is this small script that hopefully can serve you too. Here is a summary of what it does although the comments are self-explanatory:

  • Always allow requests from localhost
  • Use Redis as the data store to keep a register of requests increments per IP
  • Keep a Redis — could be a memoryregister of known malicious IPs (from previous attacks) and block them all
  • Fool the attackers by responding with HTTP status of 503 which will make them believe they succeeded in shutting down your site 😅
  • Limit the amount — stored in ENV vars — of requests (GET and POST) your users can make to certain parts of your applications like your API in a certain amount of time (period in secs).
  • Track attack patterns and be notified about them. In our case, the script enqueues a job in Sidekiq that sends alerts to the team to verify the IPs registering — as attackers — and blocking them.
# config/initializers/rack_attack.rb

# Allow localhost
Rack::Attack.safelist_ip("127.0.0.1")

# Use Redis as DS
Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new

# List of known attackers
# Support ips in the format IP a.b.c.d or with subnet a.b.c.d/f
# Ref: https://github.com/rack/rack-attack#blocklist_ipip_subnet_string
KNOWN_ATTACKERS_IP_KEY = "enter-your-id-here"
# Block them all
Redis
.new
.smembers(KNOWN_ATTACKERS_IP_KEY)
.each { |ip| Rack::Attack.blocklist_ip(ip) }

# Fool the attackers
Rack::Attack.blocklisted_responder = lambda do |request|
# Using 503 because it may make attacker think that they have successfully
# DOSed the site. Rack::Attack returns 403 for blocklists by default
[503, {}, ["Service Unavailable"]]
end

# Throttles
API_PATH = /\/api\//
Rack::Attack
.throttle("API_POST",
limit: ENV.fetch("YOUR_API_POST_LIMIT", 10).to_i,
period: 5) do |req|
req.ip if req.path.match?(API_PATH) && req.post?
end

Rack::Attack
.throttle("API_GET",
limit: ENV.fetch("YOUR_API_GET_LIMIT", 100).to_i,
period: 5) do |req|
req.ip if req.path.match?(API_PATH) && req.get?
end

#
# Set up a track to catch potential attackers
#
ATTACK_PATTERN = "ATTACK_PATTERN"
Rack::Attack
.track(ATTACK_PATTERN,
limit: ENV.fetch("YOUR_AP_REQUESTS_LIMIT", 600).to_i,
period: 60) do |req|

req.ip unless req.path.match?(API_PATH) # Already throttled
end

# Get notified
ActiveSupport::Notifications
.subscribe("track.rack_attack") do |name, start, finish, request_id, payload|
request = payload[:request]
remote_ip = request.ip

throttle_data = request.env["rack.attack.throttle_data"]
throttle_name = throttle_data.first[0]

# Only notify if this is an attack pattern
if throttle_name == ATTACK_PATTERN
# Capture insight data
data = throttle_data[throttle_name]
%i[
content_type forwarded_for fullpath request_method media_type path query_string
referer url user_agent xhr?
]
.each { |method| data[method] = request.send(method) rescue nil }
# Enqueue the notification
RackAttackNotificationWorker.perform_async(remote_ip, data.stringify_keys)
end
end

Even if you need no rate limits — just remove the throttle part of the script — it is still useful to protect your site by blocking malicious IPs while whitelisting the good ones.

Once you start collecting the attacker's IP, just add it to the Redis entry and restart your application.

> Redis.new.sadd?(KNOWN_ATTACKERS_IP_KEY, "37.212.0.0/19")

--

--

Jorge Rodriguez
Get on Board Dev

1. Software Engineer @fleetio.com 2. Co-Founder @getonbrd (500 SF)