Distributed-Denial-of-Service (DDoS)/ brute force attacks are a pain for every growing application. They will happen randomly, they will only last for a period of time, and they might not come back again for a long time. While you can’t necessarily escape them, you can take steps to reduce the impact a DDoS attack will have on your Ruby on Rails app.
More often than not, unless you run an extremely successful app, DDoS attacks are not targeted towards your Ruby on Rails application specifically (simple scripts are randomly attacking domains or IPs and are trying to bring it down "for fun").
And brute force login attacks again, might not target your user specifically (script shave a database of email and password combos that they want to try and, if there is a match, it means the password is reused and they might try to use this reused password against a larger app like email or chat).
Nevertheless, they are always a pain to deal with because those attacks will essentially send 100s-1,000s of requests per second to your site and cause disruptions to your genuine users.
At CookiesHQ, we’ve had to deal with those scenarios more than we would have liked. Once in a very complex situation, an attacker would be blocked, the IP and country would change within seconds and start the attack again. This little game of whack-a-mole lasted for 24 hours.
So we’ve decided that from day one, all the apps we build for clients would come with some level of protection against those attacks.
Today I would like to share with you the two centre piece elements we’re putting in place to reduce the impact a DDoS attack will have on our Ruby on Rails apps.
Rack attack
Rack attack is a Rack middleware for blocking & throttling abusive requests. Basically, the library sits between your requests and Rails and aims to analyse, on each request, if something out of ordinary is happening.
You can mark some IPs as safe, some IPs as not safe and dynamically modify those lists.
As with every library, the best way to understand everything it can do, is to read its documentation, for that, you can visit the Github rack attack Readme.
Rack attack is particularly useful in environments like Heroku where you don’t have access to system tools like Fail2ban or similar server-level tool that would, essentially, perform the same job in a different way.
We aim to configure our Rack Attack config per application, but our default config will look like something similar to this:
# Disable Rack Attack in development
# Allow easy switch off on production
if Rails.env.production? && ENV["DISABLE_RACK_ATTACK"].blank?
require 'ipaddr'
class Rack::Attack
class Request < ::Rack::Request
# Heroku + Cloudflare setup
# Finds the actual IP of the user connecting
def remote_ip
@remote_ip ||= (env['HTTP_CF_CONNECTING_IP'] ||
env['action_dispatch.remote_ip'] ||
ip).to_s
end
end
# Adds a certain path to a safelist.
# This path is hit at a regular interval and is behind login wall, so we assume it's ok to consider it as safe.
safelist("user data") do |req|
req.remote_ip if req.path.include?("user_data")
end
# We build a Rails app, so we assume that anyone trying to load WordPress urls too often can be safely banned.
blocklist("fail2ban") do |req|
Rack::Attack::Fail2Ban.filter("fail2ban-#{req.remote_ip}", maxretry: 10, findtime: 1.day, bantime: 1.day) do
CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
req.path.include?("/etc/passwd") ||
req.path.include?("passwd") ||
req.path.include?("wp-admin") ||
req.path.include?("wp-login") ||
/\S+\.php/.match?(req.path)
end
end
# Adds a throttle for allowed post requests on the login page within a short period of time in order to prevent brute force attacks.
throttle("limit logins per email", limit: 5, period: 20.seconds) do |req|
if req.path == "/users/sign_in" && req.post?
if (req.params["user"].to_s.size > 0) and (req.params["user"]["email"].to_s.size > 0)
req.params["user"]["email"]
end
end
end
# Adds a throttle for post requests on the sign up page within a short period of time in order to prevent SPAM signups.
throttle("limit signups", limit: 5, period: 1.minute) do |req|
req.remote_ip if req.path == "/users" && req.post?
end
# Exponential backoff for all requests to "/" path
#
# Allows 240 requests/IP in ~8 minutes
# 480 requests/IP in ~1 hour
# 960 requests/IP in ~8 hours (~2,880 requests/day)
(3..5).each do |level|
throttle("req/ip/#{level}",
limit: (30 * (2 ** level)),
period: (0.9 * (8 ** level)).to_i.seconds) do |req|
req.remote_ip unless req.path.starts_with?('/assets') || req.path.include?('/packs') || req.path.starts_with?('user_data')
end
end
# Respond with 503 status on throttle in the hope that script tool will assume they have won and leave us in peace.
self.throttled_response = lambda do |env|
[ 503, # status
{}, # headers
['']] # body
end
end
# We use Bugsnag on our app, and would like to get notified when a blockage happen.
ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, request_id, payload|
unless name.include?("safelist")
req = payload[:request]
request_headers = { "CF-RAY" => req.env["HTTP_CF_RAY"] }
info = "[Rack::Attack][Blocked] remote_ip: #{req.remote_ip}, path: #{req.path}, headers: #{request_headers.inspect}"
info_small = "[Rack::Attack][Blocked] remote_ip: #{req.remote_ip}"
Rails.logger.info info
Bugsnag.notify(name + " " + info)
end
end
end
This lives in config/initializers/rack_attack.rb
and is our reference (to be extended on an app per-app basis).
DNS level (A.K.A cloudflare)
Now, it’s possible that your Rack-attack is misconfigured, or that the attack is so big that Rack-attack is then getting in the way.
For this, I would recommend you to use a service like Cloudflare from the start.
Cloudflare will proxy your traffic through their DNS, allowing you to enable, very rapidly, the "I’m under attack" mode.
This will quickly enable the famous checking page
And will automatically vet your users on an individual basis, using captcha if needed.
Because of DNS propagation, the time and stress involved in the setup (when under active attack) I would recommend that you have Cloudflare installed and set up, your DNS migrated and proxied already. This will mean you’re ready to rack when the issue arises.
Any other tools?
As I mentioned, if you’re not hosting on Heroku, but you’re managing your own fleet of servers, then server tools like Fail2Ban can be configured and will sit outside of your app load/code.
On Heroku you can install and configure addons like Expedited Web Application Firewall or Edge
We’ve never had the need to reach for these tools and our Rack Attack + Cloudflare solution has, so far, been enough to meet our needs.
Do you need a helping hand with your Ruby on Rails app? Get in touch →