先講結論。WAF 站在請求路徑上,只看經過它的 HTTP,看到攻擊就當場擋。Suricata 站在主機 B 的實體網卡旁邊,抄一份所有進出的封包,什麼協定都看,但一個封包都不擋。兩者的位置、規則來源、看的方向都不一樣,所以互補的不是「多擋一點」,而是「WAF 看不到的地方有人在看」:WAF 放行之後的那一跳,以及主機 B 自己往外的連線。它寫下的告警與 WAF 的告警走同一條路送到主機 A,在平台上變成同一種案件。
Suricata 是 OISF(Open Information Security Foundation)維護的開源網路威脅偵測引擎,授權 GPLv2。它能當入侵偵測(IDS,只看不擋)、入侵防禦(IPS,串在線上丟封包),也能單純記錄網路活動。本安裝包只用第一種:容器映像是 jasonish/suricata:7.0(實測版本 7.0.17),以 af-packet 監聽主機 B 的實體網卡,設定檔寫死 inline: no。
引擎本身不帶判斷,判斷來自規則。安裝時下載的是 Emerging Threats Open 規則集,實測啟用 46,135 條。把這些規則依名稱分類,可以看出它主要在防什麼:
| 規則在看什麼 | 啟用條數 | 包含的類別 |
|---|---|---|
| 內部主機往外的可疑行為 | 31,920(約七成) | MALWARE、MOBILE_MALWARE(惡意程式回連)、DYN_DNS(動態網域查詢)、PHISHING(釣魚頁)、EXPLOIT_KIT(漏洞利用套件) |
| 值得知道、但不一定是攻擊 | 5,756 | INFO、HUNTING |
| 對伺服器的攻擊 | 2,556 | WEB_SPECIFIC_APPS、WEB_SERVER、EXPLOIT、SCAN |
| 攻擊成功後的回應特徵 | 645 | ATTACK_RESPONSE |
這張表是理解它角色的關鍵。WAF 的 OWASP CRS 有 850 條,全部在看「打進來的 HTTP 請求」。ET Open 的重心剛好相反,七成規則在看「裡面的主機往外做了什麼」。同一台主機上裝這兩套,重疊的部分其實不多。

圖上有三件事要看。
各種流量看得到與看不到的完整對照,在原系列第 5 篇第四節。
| 比較 | WAF | Suricata |
|---|---|---|
| 位置 | 請求路徑上 | 實體網卡旁邊 |
| 看的協定 | 只有經它轉送的 HTTP | 網卡上的所有封包;另外解讀 HTTP、DNS、TLS 握手 |
| 看的方向 | 進來的請求,以及對應的回應 | 進出都看,包含主機 B 自己往外連 |
| 規則 | OWASP CRS 850 條,描述的是通用的攻擊手法 | ET Open 四萬六千多條,多數指名特定的惡意程式、網域、漏洞 |
| 反應 | 當場回 403 | 只寫一行告警 |
| 留下的紀錄 | 只記有規則命中或回應碼異常的請求 | 告警之外,還記每條連線、每個 HTTP 交易、DNS 查詢、TLS 握手 |
| 停掉的後果 | 網站斷線 | 網站照常,少一路告警 |
由這張表可以整理出它補上的三塊:
它的代價很低。旁路的元件不會讓網站多一個故障點,這也是原系列第 9 篇的原則:線上的元件越少越好,其餘的都放到線外。
Suricata 的輸出只有一個檔案 eve.json,一行一筆 JSON。它與 WAF 的 audit.log 性質不同:audit.log 只記有問題的請求,天生是告警清單;eve.json 連正常流量也記。在一台對外服務的節點上,某次輪替後 46 分鐘內的內容是這樣:
1495 行
746 http 每個 HTTP 交易一筆
692 flow 每條連線結束時一筆
44 dns
10 tls
2 alert
1 anomaly
告警不到千分之二,其餘是網路活動紀錄。安裝包對這個檔案的分工如下:
| 誰在讀 | 拿什麼 | 用途 |
|---|---|---|
| Vector | 只取 alert,而且要有來源與目的位址 | 轉成與 WAF 事件相同的格式,送 ClickHouse 與主機 A |
| EveBox(選用) | 整個檔案 | 在瀏覽器裡翻告警與它前後的連線紀錄 |
| CrowdSec | 整個檔案 | 有掛載,但出廠沒有對應的解析器,實際不產生判斷(原系列第 2 篇第四節) |
保留期限因此分成兩種。告警進了 ClickHouse,保留 90 天。連線、DNS、TLS 這些紀錄只留在檔案裡,每小時輪替一次、保留 48 小時。事後要回頭查「那個來源當時還連了哪裡」,只有兩天的窗口。
| 步 | 在哪裡 | 做什麼 |
|---|---|---|
| 1 | 主機 B,Suricata | 規則命中,寫一行 alert。請求帶有 CF-Connecting-IP 標頭時,來源位址會被換成標頭裡的訪客位址,否則走 tunnel 的告警來源永遠是主機 B 自己 |
| 2 | 主機 B,Vector | 轉成共同格式:來源系統記為 suricata,規則編號取 signature_id,嚴重度由 Suricata 的 1(高)到 4(資訊)對映成 4 到 1 |
| 3 | 主機 B,Vector | 過濾:來源與目的都是內網的丟掉,資訊級的丟掉 |
| 4 | 主機 B,Vector | 封頂:同一條規則、同一個來源,一小時只放 1 筆(WAF 是 5 分鐘 5 筆)。被動偵測的告警重複性高,所以收得最緊 |
| 5 | 主機 B → 主機 A | od-bridge 簽章後送進平台的事件受理端點 |
| 6 | 主機 A | 依路由規則建案、併案、算風險分數,進處置流程。封鎖決策由 od-bridge 拉回,落地在 nftables 與 EDL |
兩台主機在這條線上的關係可以這樣說:主機 B 負責看與記,主機 A 負責判斷與追蹤。主機 A 不知道 Suricata 的存在,它只收到一筆來源系統是 suricata 的事件;案件頁的「跨系統關聯」會把同一個位址在 WAF 與 Suricata 各自留下的紀錄並排,這是兩個偵測系統第一次被放在一起看的地方。反過來,主機 A 停了,Suricata 照寫,告警仍然進 ClickHouse,只是沒有人把它變成案件。
告警裡的「攻擊者」是觸發規則的那個封包的來源,不一定是外面的人。主機 B 往外連而觸發的告警,攻擊者欄位就是主機 B 自己。這種案件該做的是檢查主機 B,不是封鎖來源;平台的保護清單也不會讓內網位址被封(原系列第 5 篇第三節)。
下表是一台經 Cloudflare Tunnel 對外服務的防禦節點,連續十天的全部 Suricata 告警(取自 ClickHouse,位址已去識別化)。量測當時出廠停用清單只有四條;表中的 2013504 與 2047123 在這次量測之後已加入出廠停用清單,現在安裝的節點不會再出現這兩條。同一段時間 WAF 的事件是 18 筆,其中 9 筆是被擋下的攻擊請求。
| 規則 | 告警名稱 | 筆數 | 方向 | 實際是什麼 |
|---|---|---|---|---|
| 2013504 | ET INFO GNU/Linux APT User-Agent Outbound | 545 | 主機 B → 外部 | 主機自己的套件更新檢查 |
| 2221050 | SURICATA HTTP too many warnings | 508 | 內網 → 內網 | HTTP 解讀器對同一條連線的提醒,不是特徵命中 |
| 2210044 | SURICATA STREAM Packet with invalid timestamp | 45 | 外部 → 主機 B | 主機 B 對外連線的回程封包,TCP 重組的檢查 |
| 2047123 | ET INFO Observed Cloudflare Tunneling Domain | 10 | 主機 B → 外部 | cloudflared 自己的連線 |
1,115 筆全部是低嚴重度,沒有任何一筆屬於掃描、漏洞利用或惡意程式。這個結果要分三層讀:
| 限制 | 說明 |
|---|---|
| 不擋任何封包 | 旁路模式。告警裡的 action 永遠是 allowed |
| 看不到加密的內容 | tunnel 與 HTTPS 的內文都是密文,只看得到 TLS 握手時的主機名稱與指紋 |
| 不分辨標頭可不可信 | 來源位址的覆寫是無條件的,誰帶 CF-Connecting-IP 標頭它都採信。能直接連到 WAF 埠的機器因此可以偽造告警來源,所以主機 B 用 nftables 把那個埠的來源限制在管理名單內(原系列第 8 篇) |
| 沒有記憶 | 每筆告警各自獨立。「同一個來源第幾次了」由主機 A 的併案與風險分數回答 |
| 規則不會自己更新 | 安裝時下載一次,之後要手動執行 --update-rules。安裝包沒有排程做這件事 |
| 內網對內網多半不觸發 | 多數規則限定「外部到內部」或「內部到外部」,而內部的定義(HOME_NET)預設是三段私有位址 |
容器狀態是 Up 不算證據,打 127.0.0.1 也測不到(它只看實體網卡)。最短的驗法是在主機 B 上用一個規則認得的 User-Agent 對外連一次,內容完全無害:
# 主機 B
curl -s -o /dev/null -A 'Installed OK' http://example.com/
cd /opt/integrated-waf
sudo docker compose exec -T suricata grep '"signature_id":2030880' /var/log/suricata/eve.json | tail -1 | python3 -c '
import sys, json
e = json.loads(sys.stdin.readline())
print(e["event_type"], e["src_ip"], "->", e["dest_ip"], e["dest_port"], e["app_proto"])
print(e["alert"]["signature_id"], e["alert"]["signature"], "severity", e["alert"]["severity"])
print(e["http"]["hostname"], e["http"]["http_user_agent"])'
alert 192.168.0.112 -> 104.20.23.154 80 http
2030880 ET USER_AGENTS Suspicious User-Agent (Installed OK) severity 2
example.com Installed OK
這證明 Suricata 有在抓封包、規則有載入。幾秒後同一筆應該出現在 ClickHouse,證明 Vector 有在讀這個檔案:
#### 主機 B
CH_PW=$(sudo grep '^CLICKHOUSE_PASSWORD=' .env | cut -d= -f2-)
sudo docker compose exec -T clickhouse clickhouse-client --user secstack --password "$CH_PW" --query \
"SELECT event_time, finding_rule_id, severity_id, IPv6NumToString(actor_ip) AS actor, target_host
FROM secstack.events WHERE source_system='suricata' AND finding_rule_id='2030880'
ORDER BY event_time DESC LIMIT 1"
2026-10-03 16:04:48.853 2030880 3 ::ffff:192.168.0.112 104.20.23.154
嚴重度由 Suricata 的 2 變成 3,攻擊者是主機 B 自己,目標是外部位址。這正是第五節說的出站告警的樣子。這筆事件過得了過濾,會往主機 A 送;去程那一段怎麼驗,照原系列第 10 篇第四節的做法。
沒有告警時先查兩件事:.env 的 NODE_IFACE 是不是真正在收送流量的那張網卡;suricata/rules/suricata.rules 是不是空的(安裝時規則下載失敗只會印一行警告,Suricata 仍會以空規則啟動)。