總覽的圖 0-1 用三種線型畫資料流。第一次看的人常把它讀成一條由外到內的串列:封包過了 nftables 到 WAF,過了 WAF 到 Suricata、CrowdSec、Vector,最後進平台,像一排濾網。實際上在同一條線上的只有封鎖執行點、WAF 與被保護網站,其餘元件都在另一條線。本篇說明這兩條線怎麼分、為什麼要分、分開之後付出什麼代價。前八篇講的是每一站做什麼,這一篇只回答「為什麼這樣排」。
先講結論。請求路徑是線上(inline):每個請求都要經過,當場決定放行或擋下,它停了網站就停。事件路徑與決策路徑合起來是線外(out-of-band):從 WAF 寫下的紀錄開始,不碰請求本身,慢了或停了都不影響網站。拆開的理由有六個:時間尺度、故障隔離、看得到的資訊、決定的性質、能不能丟資料、攻擊面。代價只有一個方向:封鎖來源 IP 這件事一定比攻擊晚到。
總覽列的是三條資料流。把它們依「會不會擋在請求前面」重新分組,就是兩條線:
| 總覽的資料流 | 屬於哪條線 | 在這條線上的元件 |
|---|---|---|
| 請求路徑(實線) | 線上 | Cloudflare、cloudflared、nftables(直連主機 B 的封包)、waf-nginx、被保護網站 |
| 事件路徑(藍色虛線) | 線外:同一個迴圈的去程與回程 | Suricata、Vector、ClickHouse、od-bridge 的轉送迴圈、BeakPlatform 的事件受理與建案 |
| 決策路徑(紅色點線) | 線外:同一個迴圈的去程與回程 | BeakPlatform 的簽核與決策、od-bridge 的落地迴圈、CrowdSec LAPI、EDL 檔 |

圖上最值得看的是兩個橘色方塊。兩條線之間沒有任何函式呼叫或網路請求,只靠兩種「一邊寫、另一邊讀」的東西接起來:
| 交接點 | 誰寫 | 誰讀 | 含義 |
|---|---|---|---|
| 紀錄檔 | |||
| audit.log | 線上的 WAF。判定做完、回應送出之後,才在 ModSecurity 第 5 階段寫下一行 JSON | 線外的 Vector,以檔案為來源逐行讀 | 線外拿到的永遠是「已經發生過的事」。它來不及、也沒有管道回頭改這個請求的結果 |
| 封鎖清單 | |||
| nftables blocklist、EDL 檔、CrowdSec LAPI | 線外的 od-bridge,依平台決策加入或移除元素 | 線上的封鎖執行點:主機 B 的 kernel、抓 EDL 的邊界防火牆、接 LAPI 的 bouncer | 線外影響線上的唯一方式是改這份清單。執行點查表只要一次比對,不必等任何人 |
Suricata 是第三種接法:它用 af-packet 直接抄一份實體網卡上的封包,連紀錄檔都不經過。但它只能看、不能丟包(第 2 篇第三節),所以仍然屬於線外。它不在 WAF「後面」,而是在 WAF 轉給後端的那一跳「旁邊」。
兩條線的性質對照如下,後面六節逐列展開:
| # | 線上 | 線外 |
|---|---|---|
| 處理的對象 | 眼前這一個請求 | 已經發生過的事的紀錄 |
| 時間要求 | 毫秒 | 秒到小時 |
| 停掉的後果 | 網站不通 | 網站照常、WAF 照擋;少的是建案與新的封鎖 |
| 看得到什麼 | 單一請求的完整內容 | 跨時間、跨來源的累積,加上企業自己的脈絡 |
| 決定什麼 | 這個請求放不放 | 這個來源要不要封、封多久、誰負責 |
| 誰決定 | 規則 | 人或流程 |
| 能不能丟資料 | 不能,丟了就是請求失敗 | 能,而且刻意丟(封頂) |
第 1 篇第四節的時間軸把一次 SQL injection 從頭走到尾。把它拆成兩列對齊來看,差距很明顯:
WAF 對一個請求的判定要在數十毫秒內完成,因為訪客正在等回應。「這個來源要不要封」則需要併案、算風險分數、可能還要等人看過,高危案件的 SLA 是 15 分鐘。兩件事差了好幾個數量級。
如果把它們排成一條線,也就是「平台點頭之後請求才放行」,結果只有兩種:每個請求都等平台,網站慢到不能用;或者平台被迫在毫秒內回答,那它能做的判斷就退化成查表,簽核與流程都沒有存在的空間。拆開之後,線上只做來得及做的事,需要時間的事全部移到線外,兩邊各用各的節奏。
第 5 篇第七節逐一列了各元件掛掉的後果。依兩條線重新整理,規律只有一條:
線外任何一個元件停掉,訪客感覺不到。這不是運氣,是交接點選「檔案」的直接結果:WAF 只管往檔案寫,不知道也不在乎有沒有人在讀。Vector 停一小時,WAF 不會變慢、不會排隊、不會報錯。如果交接點是一次網路呼叫(WAF 每擋一次就通知平台),平台變慢時 WAF 的工作程序就會卡在等待上,線外的故障會一路傳回線上。
反過來說,線上的元件越少越好。每多放一個元件到線上,就多一個停了會讓網站停的點。這也是 Suricata 採旁路、CrowdSec 不裝 bouncer、平台不站在請求前面的共同理由。
被保護網站剛好就是平台自己的情況。總覽提過被保護的網站可以是主機 A 上的 BeakPlatform。這時平台停了網站當然不通,但原因是它身兼「後端」,不是因為它是「管制端」。兩個角色仍然分屬兩條線,只是跑在同一台主機上。
WAF 一次判斷一個 HTTP 交易,沒有跨請求的記憶(第 2 篇第二節)。對它來說,同一個 IP 的第 1 次與第 500 次攻擊沒有差別,兩次都是「這個請求的異常分數超過門檻」。這是線上元件的本質限制:要在毫秒內回答,就只能看手上這一個請求。
「要不要封這個來源」需要的資訊恰好都不在單一請求裡:
| 需要的資訊 | 為什麼線上拿不到 | 線外怎麼拿到 |
|---|---|---|
| 這個來源過去一小時打了幾次、用了幾種手法 | WAF 不記得上一個請求 | 平台把同 IP 同規則 60 分鐘內的事件併成一案,累計事件數與風險分數 |
| 別的偵測系統有沒有看到同一個來源 | WAF 與 Suricata 互不知道對方的存在 | 兩者的告警在 Vector 統一成同一種格式,案件頁的跨系統關聯再回頭查 ClickHouse |
| 這個 IP 是不是自己人 | 防禦節點不知道哪個 IP 是辦公室出口、哪個是合作廠商 | 平台端的封鎖保護清單,寫決策前比對(第 7 篇) |
| 現在誰值班、這種案件該誰決定 | 與 HTTP 無關 | 平台的角色、班表與簽核流程 |
所以兩條線不是「同一件事做兩次」。線上回答的是「這個請求有沒有問題」,線外回答的是「這個來源有沒有問題」,兩個問題需要的輸入不同。
| # | 擋一個請求(線上) | 封一個來源(線外) |
|---|---|---|
| 影響範圍 | 這一個請求 | 這個 IP 或網段在 TTL 內的所有連線,包含正常的 |
| 判錯的後果 | 一個 403,使用者重試或回報 | 整個辦公室、整個 NAT 出口後面的人都連不上,持續到解封 |
| 多久重新判斷 | 下一個請求就重新判斷 | 要有人解封或等 TTL 到期 |
| 需要的配套 | 規則與門檻 | 保護清單、負責人、期限、紀錄、解封機制 |
| 適合誰做 | 規則,全自動 | 人,或事先定好邊界的流程 |
擋一個請求判錯了,代價小而且立刻有下一次機會,可以放心交給規則。封一個來源判錯了,代價大而且會持續,所以要有保護清單防止封到自己人、要有期限、要留下誰在什麼時候依據什麼決定的紀錄。這些配套正是平台提供的東西(第 3 篇第三節到第五節),它們全部需要時間與脈絡,只能放在線外。
線外不等於人工。出廠的小企業單人版流程在非上班時段不等人,建案後直接寫下 24 小時的封鎖決策(第 3 篇第四節)。它仍然在線外:決策一樣要過保護清單、一樣帶 TTL、一樣留紀錄、一樣由 od-bridge 拉回去落地。「自動」改變的是圖 9-2 中間那一格的長度,不是這件事在哪條線上。
Vector 送往平台的那一路有三道封頂(第 3 篇第一節):同來源同規則同 IP 的 WAF 事件 5 分鐘內最多 5 筆,Suricata 事件 1 小時內最多 1 筆,最後全域每 60 秒最多 8 筆。超過的直接丟棄,不排隊、不聚合。同一個 IP 用同一條規則打 1000 次,WAF 擋 1000 次,ClickHouse 存 1000 筆,平台 5 分鐘內只收到 5 筆。第 5 篇第五節的實測是 ClickHouse 5610 筆對平台 17 筆。
這種設計只有在線外才成立,理由有兩層:
線上完全沒有這種餘地。WAF 不能因為忙就跳過幾個請求不檢查,也不能因為忙就丟掉幾個正常請求。如果兩條線是同一條,平台的處理量上限就會變成網站的流量上限。
直接面對攻擊者輸入的程式越少越好。拆成兩條線之後:
以上六個理由都成立,但拆開不是免費的。
| 代價 | 說明 |
|---|---|
| 封鎖一定比攻擊晚 | 圖 9-2 的括號那一段。從第一個攻擊請求到來源被封,最快是數秒(全自動流程),慢則到人處理完為止。這段時間的防護完全靠 WAF 逐請求擋 |
| 線外只能封來源,不能補擋請求 | WAF 規則沒涵蓋到的攻擊請求,已經到了後端。Suricata 可能在 WAF 轉給後端的那一跳看到它並產生告警,平台可以據此建案、封來源,但那個請求已經結束。線外補的是「下一次」,補不了「這一次」 |
| 線外看到的是線上願意寫下來的 | WAF 的 audit engine 設成 RelevantOnly,只記有規則命中或回應碼 4xx/5xx 的交易(第 2 篇第二節)。沒命中任何規則的請求在 audit.log 裡不存在,線外無從得知 |
| 回程的落點受部署限制 | 線外把決策寫進封鎖清單,但清單要有執行點去讀才有用。對外走 Cloudflare Tunnel 時,訪客的封包不經過主機 B 的實體網卡,nftables blocklist 看不到他們(第 5 篇第二節、第 8 篇第一節)。要讓迴圈對 Internet 上的攻擊者真正閉合,得把 EDL 接到邊界設備(第 6 篇) |
| 兩條線各有一份數字 | 平台的事件是封頂後的子集。「今天被打幾次」要查 ClickHouse,「有誰在打、處理了沒」才看案件(第 5 篇第五節)。拿案件數當攻擊次數會嚴重低估 |
第一項代價是可以接受的,前提是 WAF 本身擋得住。兩條線的分工建立在一個假設上:線上的逐請求阻擋是主要防線,線外的封鎖來源是加強。如果 WAF 的規則被關掉、被設成只偵測不阻擋(WAF_RULE_ENGINE=DetectionOnly),封鎖延遲那一段就沒有任何東西在擋。導入初期常用只偵測模式觀察誤判,那段期間要清楚知道自己處在這個狀態。
維運面的代價(多一台主機、要設定保護清單與角色)在第 4 篇第五節,這裡不重複。
| 誤解 | 實際 |
|---|---|
| 封包過了 WAF 之後才到 Suricata、CrowdSec | 兩者都不在請求路徑上。Suricata 抄一份實體網卡上的封包,CrowdSec 讀紀錄並當作決策的落地點。請求過了 WAF 之後的下一站是被保護網站 |
| 平台慢了或掛了,網站會跟著慢或不通 | 不會。WAF 只往檔案寫紀錄,不等平台回應。例外是被保護網站剛好就是平台自己,那是因為它是後端(第三節) |
| 平台沒收到的事件,就是沒擋到的攻擊 | 封頂丟掉的是通知。每一筆都已經被 WAF 當場擋下,ClickHouse 也留有全量 |
| 案件還沒簽核之前,攻擊者可以繼續打進來 | 簽核決定的是要不要封來源。每個攻擊請求仍然各自被 WAF 當場判定,與案件進度無關 |
| 封鎖決策落地之後,這個攻擊者的請求就進不來了 | 要看他從哪條路來、清單由誰在讀。走 Cloudflare Tunnel 的流量 nftables 看不到,仍由 WAF 逐請求擋,除非邊界設備有抓 EDL |
| 接上平台之後 WAF 會多擋一些攻擊 | 不會。線外不改變線上的規則與門檻(第 4 篇第一節)。平台改變的是擋下之後有沒有人負責、要不要封來源 |
| 線外的處置都要等人 | 流程可以全自動。是否自動只影響線外那一段花多久,不影響它在哪條線上 |
| 事件路徑與決策路徑是兩套獨立的機制 | 是同一個迴圈的去程與回程,兩端都由 od-bridge 負責,連線都由主機 B 發起 |