Ograniczanie tempa i zapobieganie nadużyciom z Rack::Attack
Każda publiczna aplikacja jest tłuczona — credential-stuffing na loginie, scrapery na katalogu, brute force na API. Deep-dive o Rack::Attack: throttling, blokowanie i śledzenie w stylu fail2ban, zastosowane na właściwej warstwie, by trzymać nadużycia z dala od aplikacji.
W chwili, gdy aplikacja jest publiczna, jest celem. Boty próbują skradzionych haseł na twoim loginie, scrapery ściągają twój katalog tak szybko, jak mogą, ktoś brute-force’uje twoje API albo endpoint resetu hasła. Nic z tego nie jest egzotyczne — to stały szum tła internetu — a aplikacja bez obron będzie w końcu miała awarię, wyciek albo skradziony zbiór danych z tego powodu. Rack::Attack to standardowa odpowiedź Ruby: kawałek middleware Rack, który throttluje, blokuje i śledzi nadużywające żądania zanim dotrą do twojej aplikacji. Oto jak go dobrze używać.
Dlaczego middleware to właściwa warstwa
Przypomnij sobie dyskusję o middleware Rack: middleware działa przed twoimi kontrolerami, widzi każde żądanie i może je zwrzeć bez dotykania aplikacji. To dokładnie tam należy zapobieganie nadużyciom. Chcesz odrzucić tysięczną próbę logowania z jednego IP zanim uruchomi kontroler, uderzy w bazę i zużyje workera — całym sensem jest tanie zatrzymanie nadużyć, na krawędzi, a nie wpuszczenie ich i kosztowna obsługa. Rack::Attack to middleware właśnie z tego powodu i używa składu cache (Redis albo Memcached), by liczyć żądania przez twoje serwery, by limity trzymały dla całej floty, nie per-proces.
# config/initializers/rack_attack.rb
Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
Throttling: podstawowe narzędzie
Koniem roboczym jest throttle — pozwól na N żądań na okres na klucz, a resztę odrzuć. Definiujesz, co liczyć i jak kluczować:
# ogólny ruch: 300 żądań / 5 min na IP
Rack::Attack.throttle("req/ip", limit: 300, period: 5.minutes) do |req|
req.ip
end
# próby logowania: 5 na 20s na IP — obrona przed credential stuffing
Rack::Attack.throttle("logins/ip", limit: 5, period: 20.seconds) do |req|
req.ip if req.path == "/login" && req.post?
end
# próby logowania na KONTO, nie tylko IP — zatrzymuje rozproszone ataki na jednego użytkownika
Rack::Attack.throttle("logins/email", limit: 5, period: 20.seconds) do |req|
req.params["email"].to_s.downcase.presence if req.path == "/login" && req.post?
end
Blok zwraca dyskryminator — klucz, po którym liczyć — albo nil, by pominąć. Dyskryminator to
miejsce, gdzie idzie myślenie. Throttling loginów po IP zatrzymuje jedną maszynę cię tłukącą; ale
atakujący z botnetem rozsmarowuje próby przez tysiące IP, więc throttlujesz także po celu
(emailu, którego próbują), co łapie rozproszony atak na jedno konto, który limity per-IP by
przegapiły. Dobre ograniczanie tempa zwykle oznacza kilka throttli kluczowanych różnie, każdy
łapiący kształt ataku, którego inne nie.
Blocklisty i safelisty
Poza throttlingiem możesz wprost zablokować albo dopuścić żądania:
# zawsze dopuść swoje IP biura / monitoringu — nigdy nie throttluj samego siebie
Rack::Attack.safelist("allow/office") { |req| req.ip == "203.0.113.4" }
# blokuj znanych złych aktorów wprost
Rack::Attack.blocklist("block/bad-ip") { |req| BLOCKED_IPS.include?(req.ip) }
# blokuj oczywiste śmieci — skanery sondujące /wp-admin, .php itd.
Rack::Attack.blocklist("block/scanners") do |req|
req.path =~ /\.(php|asp)$|wp-admin|wp-login/
end
Safelisty działają pierwsze i zwalniają zaufane źródła (kluczowe — nie chcesz, by throttle zablokował twój własny monitoring albo biuro podczas incydentu). Blocklisty odrzucają znany-zły ruch przed jakimkolwiek liczeniem throttle, co jest najtańszym możliwym odrzuceniem.
Śledzenie w stylu fail2ban: eskalująca odpowiedź
Najpotężniejszym wzorcem jest Fail2Ban: śledź wzorzec podejrzanego zachowania i, gdy źródło przekroczy próg, zablokuj je na dłuższy cooldown. Tak odpowiadasz atakującemu, a nie serii — pojedynczy użytkownik, który raz potknie się o limit, dostaje krótki throttle, ale źródło, które wielokrotnie sonduje, zostaje zbanowane wprost na jakiś czas:
Rack::Attack.blocklist("fail2ban/probes") do |req|
Rack::Attack::Fail2Ban.filter("probes-#{req.ip}", maxretry: 5, findtime: 10.minutes, bantime: 1.hour) do
# "true" = to żądanie wygląda złośliwie (np. uderzanie w znane ścieżki ataku)
suspicious_path?(req.path)
end
end
Pięć podejrzanych żądań w dziesięć minut zarabia godzinny ban. Ta eskalacja — lekkie dotknięcie za sporadyczne potknięcie, ciężkie za uporczywe nadużycie — jest tym, co oddziela mądrą obronę od tępej, która irytuje też prawowitych użytkowników.
Odpowiadaj rozważnie
Gdy throttlujesz kogoś, to, co zwracasz, ma znaczenie. Właściwym statusem jest 429 Too Many
Requests, idealnie z nagłówkiem Retry-After, by dobrze zachowujące się klienty wiedziały, by
się wycofać, i jasnym komunikatem dla ludzi, którzy trafili na limit prawowicie:
Rack::Attack.throttled_responder = lambda do |req|
[429, { "Content-Type" => "text/plain", "Retry-After" => "20" }, ["Za dużo żądań. Poczekaj.\n"]]
end
Zwrócenie porządnego 429 (nie generycznego 500) jest zarazem poprawne i uprzejmiejsze — prawowity użytkownik, który krótko trafia na limit, dostaje uczciwe „zwolnij”, a uprzejmy bot albo klient API wycofuje się automatycznie.
Strój, by uniknąć fałszywych alarmów
Realna ostrożność: limity tempa zbyt agresywne blokują prawowitych użytkowników — ruchliwe biuro za jednym NAT IP, sieć mobilną dzielącą adresy, użytkownika mocy. Ustaw limity dość hojnie, by normalne użycie nigdy ich nie potknęło, a nadużycia tak, monitoruj, co throttlujesz (Rack::Attack odpala powiadomienia, które możesz logować), i wypatruj fałszywych alarmów po wdrożeniu nowych reguł. Zacznij zachowawczo, obserwuj i zaciskaj — ogranicznik tempa, który blokuje prawdziwych klientów, jest gorszy niż nadużycie, które miał zatrzymać.
Werdykt
Każda publiczna aplikacja jest pod stałym niskopoziomowym atakiem, a Rack::Attack to właściwe
narzędzie, by go obsłużyć na właściwej warstwie — middleware, które throttluje, blokuje i banuje
nadużywające żądania, zanim kosztują cię kontroler, zapytanie albo workera. Throttluj wrażliwe
endpointy (login, signup, reset hasła, API) kluczowane na kilka sposobów — po IP i po celu — by
łapać zarówno serie, jak i rozproszone ataki; safelistuj własną infrastrukturę; blocklistuj
oczywiste śmieci; i używaj eskalacji w stylu Fail2Ban, by karać uporczywość, a nie sporadyczne
potknięcie. Odpowiadaj porządnym 429 i Retry-After, wesprzyj to współdzielonym składem Redis,
by limity trzymały przez flotę, i strój ostrożnie, by nie blokować prawdziwych użytkowników. To
mały inicjalizator, który zamienia stałe nadużycie tła internetu z zagrożenia w nie-zdarzenie.