基本上開發 Rails 有在接 error monitoring(Airbrake、Rollbar)或是最基本有在看 Log 的應該都會知道網站一上線後,就會有些不尋常的流量,像是會有人來猜後台網址、夾帶和一般使用者不一樣的 params
通常這些人可能會是幾種
- 惡意機器人,找到 domain 就 try 常見漏洞
- 盯上你網站的駭客
像我自己 rollbar 的後台就可以常收到類似
- /.well-known/security.txt
- /app-ads.txt
- /api/v1/pod
甚至有人會嘗試大量請求 API,可能想看看能不能搞垮你的機器之類的?
那麼身為網站開發者的我們當然不希望因為這些流量而增加成本或是倒站的風險,這時候就可以把一些比較誇張的行為檔下。
我們可以用 Rack::Attack 來做一些基本的防禦。
直接加到 Gemfile
gem "rack-attack"
bundle install 後在 config/application.rb
檔案內指定 middleware 要過 Rack::Attack
config.middleware.use Rack::Attack
接下來新增 config/initializers/rack_attack.rb
我們會把一些過濾和設定放在這裡
Rake::Attack 預設會直接把這些流量記錄在 Rails.cache 內,所以如果你要額外設定在 Redis 內可以寫入
不過如果你的 Rails.cache 也是 Redis 基本上就可以省掉下面這些,除非你要分另一台 redis
redis_client = Redis.connect(url: ENV["REDIS_URL"])
Rack::Attack.cache.store = Rack::Attack::StoreProxy::RedisStoreProxy.new(redis_client)
基本防範
接下來做基本的使用
class Rack::Attack
# 每個 IP 在 1 分鐘內不能刷超過 40 次,否則會被暫時鎖定
throttle("req/ip", limit: 40, period: 1.minute) do |req|
req.remote_ip if req.path == "/"
end
# 路由 /api/v1 系列的限制 1 分鐘內請求 200 次
throttle("api_v1_requests", limit: 200, period: 1.minute) do |req|
if req.path.start_with?("/api/v1")
req.ip
end
end
# 2 分鐘內透過 /users/sign_up 發出 post 超過 10 次,直接鎖 60 分鐘
Rack::Attack.blocklist('allow2ban sign up scrapers') do |req|
Rack::Attack::Allow2Ban.filter(req.ip, maxretry: 10, findtime: 2.minutes, bantime: 60.minutes) do
req.path == '/users/sign_up' and req.post?
end
end
end
客製化回應
HTTP 429 狀態碼的意思是 Too Many Requests,通常回這個就可以了,如果想要更清楚的或是給 api 使用者看的可讀訊息,就可以透過 throttled_response
自定義
class Rack::Attack
Rack::Attack.throttled_response = lambda do |env|
match_data = env['rack.attack.match_data']
now = Time.zone.now.to_i
seconds_before_next_try = match_data[:period] - now % match_data[:period]
error_message = "Throttled, please try after #{seconds_before_next_try} seconds\n請求過於頻繁,請在 #{seconds_before_next_try} 秒之後嘗試"
req = Rack::Attack::Request.new(env)
if req.api_request?
content_type = "application/json;charset=utf-8"
response_body = {
code: 1234,
errors: {
message: error_message
}
}.to_json
else
content_type = "text/plain;charset=utf-8"
response_body = error_message
end
headers = {
'X-RateLimit-Limit' => match_data[:limit].to_s,
'X-RateLimit-Remaining' => '0',
'X-RateLimit-Reset' => (now + seconds_before_next_try).to_s,
'Content-Type' => content_type
}
[429, headers, [response_body]]
end
end
安全名單
如果你有自己公司內網或是絕對不想被檔的 IP,可以放在 safelist
class Rack::Attack
class Request < ::Rack::Request
def allowed_ip?
allowed_ips = ["127.0.0.1", "::1"]
allowed_ips.include?(remote_ip)
end
end
...
...
# Do not throttle for allowed IPs
safelist('allow from localhost') do |req|
req.allowed_ip?
end
end
在 Proxy 之後
如果你的流量前面有一台 proxy 或是 Cloudflare,要更有效防止 DDOS 的話,必須知道他的真正 IP,所以我們可以 overwrite Rack::Attack 內的 remote_ip
class Rack::Attack
class Request < ::Rack::Request
def remote_ip
# Cloudflare stores remote IP in CF_CONNECTING_IP header
@remote_ip ||= (env['HTTP_CF_CONNECTING_IP'] ||
env['action_dispatch.remote_ip'] ||
ip).to_s
end
end
end
記錄被檔下的流量
為了避免我們真的誤判,或是想要研究到底是哪些人被檔 Rack::Attack 也支援 track
ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
if req.env["rack.attack.match_type"] == :throttle
Rails.logger.info "[Rack::Attack][Blocked]" <<
"remote_ip: \"#{req.remote_ip}\"," <<
"path: \"#{req.path}\", " <<
"headers: #{request_headers.inspect}"
end
end
記錄下來觀察幾天,就可以在 log 裡面用 grep 找想找的資訊了
grep -c "max_req" log/production.log
grep -c "allow2ban" log/production.log
grep -B 3 -A 2 "pentesters" puma.log