Copy
You're reading the Ruby/Rails performance newsletter by Speedshop.

Do you have extra learning/education budget leftover in 2022? Consider buying one of my books or my workshop!

Looking for an audit of the perf of your Rails app? I've partnered with Ombu Labs to do do just that.

Why use a Web Application Firewall (WAF)? Isn't rack-attack good enough?

I got this question from a reader in response to my last newsletter:
 
"I'm curious what cloudflare does that rack-attack can't do. Do you have any clue around this and if it would be possible to improve rack-attack to make it work as well as cloudflare's WAF.
-Sammy"


Well, this is a great question, and one I think we can answer pretty well with a bit of understanding what a Distributed Denial of Service (DDoS) is, and a bit of Little's Law (woo!).

So first, let's understand the nature of the threat. A DDoS is simply a lot of traffic to your app distributed across a large number of IP addresses. The "lots of traffic" part is what takes your site down. You're overwhelmed by the sheer amount of requests, something in the infrastructure breaks (usually the databases, or you simply can't bring up enough web server capacity) and the site effectively goes down and becomes inaccessible. The "distributed" part is what makes it hard to block. The huge number of IP addresses that this traffic is coming from makes it difficult to distinguish legit traffic from attacker traffic. If it was just one IP address, then you could just ban that IP and be on your merry way. But there's just too many IPs in a DDoS attack for ban strategies to be very effective.

Let's say we have 10 Ruby processes worth of capacity. Let's assume a request to our application takes 100ms to process.

Now, assuming no other traffic, we can gauge via Little's Law just how much DDoS traffic we can handle. Thanks to Little's Law, we can know that 100 requests per second will utilize all available capacity, because 100 requests per second * 0.1 seconds per request = 10, the same as the amount of processes we have available. The app will probably get unusably slow a little bit before this, at around 80 to 90 requests per second, but it's a useful estimate.

A DDoS attack basically boils down to this equation: are you providing more capacity than the attacker is using (and therefore denying to legitimate users?).

Let's say you want to use rack-attack to stop this DDoS and you can't bring up any more capacity, so you're stuck with 10 Ruby processes.

The first problem you've got is: what rule do you write to ban DDoS traffic? 

Assuming you can't easily identify any patterns in the traffic that would make it easily bannable, like a specific URL that the traffic is requesting or a specific user-agent header it sends along or something like that, you're stuck banning user agents based on how many requests per second they're sending.

You can do that in rack-attack like this:
Rack::Attack.throttle("requests by ip", limit: 5, period: 2) do |request|
  request.ip
end
That would be 5 requests per 2 minutes. OK, fine. But let's say the DDoS attacker has 12,000 IP addresses to send attack traffic from. If they have 12,000 IPs, then each IP will only send 1 request per 2 minutes to your application. Your rack-attack rule will never be tripped!

Basically, rack-attack fails as a DDoS protector because the amount of information in the Rack environment that you can use to identify a request as an attacker or as legitimate is extremely limited - too limited to block the attack.

Even if your throttling rule does work (let's say they've only got 5 IPs or something, although that would never happen IRL) you still have another problem: you still have to run Ruby and make a call to Redis to actually ban the traffic.

Let's say that loading up your Rack middleware stack, running rack-attack, getting the Redis result which contains the throttling data, and returning a response takes 10 milliseconds for the "banned/attacker" case. That means your DDoS attacker is now using 1 process (0.01 seconds per request * 100 requests per second) instead of all 10. You're good right? Well, yes, you are, until your attacker just increases the amount of traffic they're sending and you're back to square 1.

WAFs do two things very well against DDoS attacks that rack-attack cannot:

1. WAFs have access to huge pools of internet traffic, not just traffic from your app, so they can identify bot pools and suspicious behavior from past attacks on other applications, not just yours. rack-attack only has the Rack environment (IP, request headers, etc) to make the ban decision -  a WAF has that plus years of history for this IP address.
2. WAFs block DDoS traffic quickly and efficiently on their own servers, not on your servers. Instead of the DDoS attacker having to send enough traffic to overwhelm your 10 Ruby processes, they have to send enough traffic to overwhelm all of Cloudflare, which is much, much harder to do.

This isn't a design problem with rack-attack. You couldn't write a better version of rack-attack which fixes these issues. It's a byproduct of where rack-attack sits in the stack: in your app, on your servers, with a limited amount of information and resources.

So, as I said before: if you have something worth protecting, put it behind a WAF. Here's a shortlist of vendors in this space:

1. Cloudflare
2. Each major cloud provider has their own, e.g. Amazon WAF.
3. Akamai Kona

Until next year,

-Nate
 
You can share this email with this permalink: https://mailchi.mp/railsspeed/why-use-a-waf-what-rack-attack-can-and-cant-do-against-ddos?e=[UNIQID]

Copyright © 2022 Nate Berkopec, All rights reserved.


Want to change how you receive these emails?
You can update your preferences or unsubscribe from this list.