這篇WAF是由三個repo組成,會分五篇說明,偏重與BeakPlatform的關係
防禦節點(主機 B)上那個叫 waf-nginx 的容器裡裝的是什麼、它在整套架構裡負責哪一段、不負責哪一段。本系列共五篇,本篇是總覽。Suricata 與 CrowdSec 另有專篇,這裡只在需要對照時提到。
閱讀順序
1. 總覽:一個容器、三個名字、一條單向的線(本篇)
2. 元件與規則引擎:nginx、ModSecurity v3、OWASP CRS 各做什麼,設定怎麼疊起來,分數怎麼算
3. 一個請求在 WAF 容器內的旅程:五個階段、403 由誰回、轉給後端時帶什麼、為什麼 real_ip 刻意失效
4. audit.log:什麼時候寫、一行長什麼樣、Vector 怎麼把它變成平台事件
5. 設定、調校與驗證:可調的參數、誤判怎麼排除、怎麼確認它真的在擋、掛了會怎樣
上一層是 防禦節點運作原理(九篇),本系列的主機、IP 與線型慣例都沿用那一套:主機 A(192.168.0.111)是 BeakPlatform,主機 B(192.168.0.112)是防禦節點,攻擊者用文件保留位址 203.0.113.42。
在整條鏈上,WAF 是唯一站在請求路徑上、讀得到完整 HTTP 內容、而且能當場把請求擋下來的元件。nftables 只看 IP 與埠,Suricata 只能旁觀不能丟包,CrowdSec 看的是時間軸上的行為,平台則要等事件送到才知道有事。攻擊者送出 ?id=1' OR 1=1-- 的那幾十毫秒內,唯一做出反應的就是這個容器:回 403,請求不再往後走。
但它的責任也在這裡結束。擋掉之後它留下的只有 audit.log 裡的一行 JSON,「這個 IP 是不是累犯」「要不要封它」「封多久」「誰負責」這些問題它一概不知道,也沒有機制去知道。這些是平台補上的部分,不是 WAF 做不好。
讀者常把「WAF」當成一個軟體,實際上 waf-nginx 容器裡是三個各自獨立的專案疊在一起,分工非常清楚。下表的版本是本文撰寫時(2026-09-26)在防禦節點上實測的;映像檔 owasp/modsecurity-crs:nginx 是浮動 tag,安裝時拉到什麼版本就是什麼版本,第 4 篇會談這件事的後果。
| 名字 | 實測版本 | 它是什麼 | 類比 |
|---|---|---|---|
| nginx | 1.30.5 | Web 伺服器兼反向代理。收 TCP 連線、解析 HTTP、把請求轉給被保護網站、把回應送回去。它本身不懂什麼叫攻擊 | 門口的櫃台:收信、轉交、回信 |
| ModSecurity v3(libmodsecurity 3.0.16 + ModSecurity-nginx connector 1.0.4) | 3.0.16 / 1.0.4 | 規則引擎。提供一套規則語言(SecRule)、變數、轉換函式、運算子與動作,並在請求與回應的固定階段執行規則。它本身不帶任何規則,空跑時什麼都不擋。connector 是把引擎掛進 nginx 生命週期的膠水 | 安檢儀器:有掃描能力,但要有人告訴它什麼算違禁品 |
| OWASP Core Rule Set(CRS) | 4.25.1,850 條 | 規則內容。SQL injection、XSS、命令注入、路徑穿越、掃描器指紋、協定畸形、回應外洩等的偵測規則,加上一套「異常分數」計分與攔截機制。CRS 是社群維護的通用規則,不認識任何特定應用 | 違禁品清單與判定門檻 |
同一個 CRS 也能跑在 Coraza、Apache 的 mod_security2 或其他相容引擎上;同一個 libmodsecurity 也能載入自己寫的規則。三者是可以替換的,本安裝包選的是 OWASP 官方維護的 nginx 版容器映像,理由是零編譯、設定全部靠環境變數、規則與引擎版本由上游一起配好。
下圖只畫與 WAF 直接相連的東西。實線是請求路徑,藍色虛線是事件路徑,紅色點線是決策路徑。注意紅線沒有任何一條回到 WAF。
| 事情 | WAF 做嗎 | 說明 |
|---|---|---|
| 判斷單一請求是不是攻擊 | 主責 | 依 CRS 規則對請求與回應打分數,達門檻即攔下。這是整條鏈唯一的即時阻擋點(走 Cloudflare 時 edge 另有一層,不屬本安裝包) |
| 回 403 給攻擊者 | 主責 | 由 nginx 回,後端完全不知道有這個請求 |
| 留下證據 | 主責 | audit.log 一行 JSON:命中的規則、分數、請求標頭、方法、URI、直連來源 |
| 把請求轉給被保護網站 | 主責 | 放行的請求由 nginx proxy_pass 到 WAF_BACKEND_URL,並補上 X-Forwarded-* 標頭 |
| 判斷「攻擊者真實 IP」是誰 | 不做 | WAF 記的是直連它的對象(cloudflared 容器或 docker 網關)。要不要相信 CF-Connecting-IP 是 Vector 的事,理由見第 2 篇第六節 |
| 記得「這個 IP 剛才已經被擋 50 次」 | 做不到 | WAF 沒有跨請求記憶,每次都當第一次。累犯判斷由平台的併案計數與風險分數補上 |
| 封鎖來源 IP | 做不到 | 它只能逐請求 403。封 IP 是平台決策 → od-bridge 落地到 nftables/CrowdSec/EDL,WAF 不在落地點清單裡 |
| 速率限制、登入暴力破解偵測 | 沒有 | CRS Paranoia Level 1 沒有速率規則;本安裝包也沒開 nginx 的 limit_req。這是已知缺口,見第 4 篇第六節 |
| TLS 終結 | 不做 | 容器有 8443 監聽(自簽憑證)但 compose 沒有發布它。走 Cloudflare 時 TLS 在 edge 終結,tunnel 內是明文 HTTP 進 WAF |
| 決定哪些事件要送去建案 | 不做 | WAF 只管寫 log。過濾、歸因、封頂全在 Vector |
| 更新規則 | 不會自動 | 規則跟著映像檔走,重拉映像才會換版本 |
圖 0-1 值得盯著看的是線的方向。WAF 與平台之間只有 audit.log 這一條線,方向是 WAF → Vector → od-bridge → 平台。平台不會回頭做任何事:不會改 WAF 的規則、不會把封鎖清單推回 WAF、不會叫 WAF 放行某個 IP。三個直接推論:
| 推論 | 後果 |
|---|---|
| 平台掛了,WAF 照擋 | 攻擊當下的阻擋 100% 在防禦節點內完成,不依賴主機 A 活著。平台不在時損失的是「擋掉之後」的那些事:沒有案件、沒有封鎖、沒有解封 |
| 平台核可的封鎖,對「經 Cloudflare 進來的請求」在 WAF 這一層沒有效果 | 封鎖落地在 nftables input hook,但 tunnel 流量是 cloudflared 主動連出的既有連線,封包來源是 Cloudflare。被封的 IP 再打一次,還是由 WAF 逐請求判斷、逐請求 403。要在邊界擋掉得靠 EDL 接外部防火牆(原系列第 5、6 篇) |
| WAF 誤判時,要改的是 WAF 自己的覆寫檔,不是平台 | 平台上把案件標成誤判、放行,都不會讓下一個同樣的請求通過 WAF。規則排除寫在主機 B 的 waf/modsec-overrides/modsecurity-override.conf(第 4 篇第二節) |
設定了歡迎頁(WELCOME_HOSTNAME)時,compose 會多起兩個容器:welcome(一頁靜態 HTML 的 nginx)與 waf-welcome(用同一個映像、同一組參數的第二個 WAF,後端指向 welcome)。cloudflared 的 ingress 依 hostname 或路徑前綴決定送哪一個。這樣做的目的是讓「刺探根路徑與亂猜的路徑」也經過 WAF 而產生事件,而不是直接 404 掉。兩個容器的差別只有三處:
| 項目 | waf-nginx(主站) | waf-welcome(歡迎頁) |
|---|---|---|
| 後端 | WAF_BACKEND_URL(被保護網站) | http://welcome:80(容器內靜態頁) |
| audit log | waf/log/audit.log | waf/log/audit-welcome.log |
| 事件的 target.service | waf-nginx | waf-welcome(Vector 依檔名判斷) |
| 內網驗證埠 | 8080 | 8082 |
面各篇講的規則、階段、log 格式對兩個容器完全相同,不再分開說。
| 本系列 | 原系列(防禦節點運作原理)已經談過的部分 |
|---|---|
| 第 1 篇 元件與規則引擎 | 第 2 篇第二節的規則群表;本篇把引擎、載入順序、計分方式補齊 |
| 第 2 篇 請求旅程 | 第 1 篇第 4 站;本篇把五個階段、轉送標頭、real_ip 的設計理由展開 |
| 第 3 篇 audit.log | 第 3 篇第一節的 Vector 管線;本篇從 log 本身的格式與寫入條件出發 |
| 第 4 篇 設定與驗證 | 安裝章節第七節的日常操作;本篇只談 WAF 相關的參數、排除與故障 |