惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Cloudflare Blog
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
博客园 - 司徒正美
V
Visual Studio Blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
月光博客
月光博客
aimingoo的专栏
aimingoo的专栏
博客园_首页
Blog — PlanetScale
Blog — PlanetScale
博客园 - 聂微东
S
SegmentFault 最新的问题
T
The Blog of Author Tim Ferriss
D
Docker
Vercel News
Vercel News
Recent Announcements
Recent Announcements
Last Week in AI
Last Week in AI
爱范儿
爱范儿
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏

Nic Lin's Blog

謝明真 - 高效領導力的課後筆記 NFT 開發實戰!基礎智能合約入門 (3) NFT 開發實戰!基礎智能合約入門 (2) NFT 開發實戰!基礎智能合約入門 (1) 如何自我檢測 log4j CVE 漏洞 Rails 如何在資料寫入時記錄來源 IP 位置 如何經營工程師 Youtube 頻道 - Part 8 營收篇 如何經營工程師 Youtube 頻道 - Part 7 酸民文化篇 如何經營工程師 Youtube 頻道 - Part 5 設備器材篇 如何經營工程師 Youtube 頻道 - Part 4 後製剪輯篇 如何經營工程師 Youtube 頻道 - Part 3 文案企劃篇 如何經營工程師 Youtube 頻道 - Part 2 設備器材篇 如何經營工程師 Youtube 頻道 - Part 1 制訂頻道方向篇 如何經營工程師 Youtube 頻道 - Part 0 Rails 中避免 race condition 的最佳實踐(二) Rails 中避免 race condition 的最佳實踐(一) 10 分鐘整合 google sheet 做自動化開發功能週報 經營 Side Project 300 天所帶來的收穫及挑戰 我的 Youtube 影片製作流程 API 設計時必須注意的 HTTP header 底線問題 如何提升你的程式可讀性之實務技巧(三) 如何提升你的程式可讀性之實務技巧(二) 如何提升你的程式可讀性之實務技巧(一) Ruby 中使用 freeze 優化效能的時機 避免 React 中的 useEffect 無限 render 在 Rails 內輕量使用 Vue Component 的最佳實踐 如何在區域網路用 Docker 架設有 SSL 的 Gitlab 從被問到問人,那些我常問的面試問題 [Rails] 如何漂亮寫出可維護的 query (Maintainable Rails Query) 在已知長度情況下優化 slice 的性能
如何用 Rack::Attack 阻擋 DDOS / 惡意流量
Nic Lin · 2019-08-16 · via Nic Lin's Blog

基本上開發 Rails 有在接 error monitoring(Airbrake、Rollbar)或是最基本有在看 Log 的應該都會知道網站一上線後,就會有些不尋常的流量,像是會有人來猜後台網址、夾帶和一般使用者不一樣的 params

通常這些人可能會是幾種

  1. 惡意機器人,找到 domain 就 try 常見漏洞
  2. 盯上你網站的駭客

像我自己 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

參考資源