防禦節點(主機 B)上除了 WAF,還裝了幾個別人寫的系統。這一系列每篇只談一個,不深入它的設定細節,只回答三件事:它是什麼、它在主機 A 與主機 B 之間佔哪一段、少了它整體會缺什麼。本篇是 ClickHouse。
先講結論。WAF、Suricata、CrowdSec 的告警,送到主機 A 之前會先篩掉一部分、再限制數量,主機 A 上的案件因此只是其中一小部分。ClickHouse 存的是篩選之前的那一份,每筆告警一列,留 90 天。它只有一個寫入者,就是 Vector。它不存封鎖決策,不參與任何判斷,停掉不影響 WAF 擋人,也不影響主機 A 建案。出廠時沒有任何程式固定去讀它:要看,就自己下 SQL;主機 A 的案件頁可以回查它,但要多做一步設定。
ClickHouse 是一套開源的欄式資料庫,最早由 Yandex 開發,2016 年開源,現在由 ClickHouse 公司維護,授權 Apache 2.0。本安裝包用的容器映像是 clickhouse/clickhouse-server:24.8。
「欄式」指的是儲存方式。一般的資料庫把一筆資料的所有欄位放在一起,適合「把這張訂單整筆拿出來改」。事件記錄的問法不一樣:「過去一天各來源各幾筆」「這個位址出現過幾次」,每次只碰兩三個欄位,卻要掃過很多筆。欄式資料庫把每個欄位分開存,查詢只讀用得到的欄位;同一欄的值性質相近,壓縮率也高。在一台實測的節點上,事件表壓縮後約為原本的六分之一。
它在本安裝包裡的樣子很單純:
| 項目 | 內容 |
|---|---|
| 資料庫 | secstack,只有這一個 |
| 資料表 | events,一筆告警一列,第三節逐欄說明 |
| 彙總 | events_per_minute,記錄每分鐘、每個來源各幾筆,其中高嚴重度幾筆。事件寫入時自動累加,不必另外排程 |
| 帳號 | secstack 一個。密碼在安裝時隨機產生,存在主機 B 的 .env |
| 連接埠 | 8123(HTTP,附一個網頁查詢介面)與 9000(原生協定) |
| 誰能連 | 主機 B 本機、主機 B 上的容器、安裝時 --admin-ips 列出的位址、主機 A |
| 保留期限 | 90 天,依事件發生的時間計算,到期自動刪除 |
「誰能連」這一項的做法和其他管理介面不同。Grafana、EveBox 那些埠是由主機 B 的防火牆限制來源;ClickHouse 的兩個埠沒有經過那一層,限制是寫在帳號上的。不在名單上的位址連得到埠,但登入會被拒絕。

圖上有三件事要看。
三個來源的告警都寫進同一張 events,一筆告警一列。這種把所有欄位攤平放在同一張表的做法,通常叫寬表:欄位多,但查詢時不必把幾張表接起來。它有 20 個欄位,橫著排讀不了,這裡轉成一個欄位一列:
| 欄位 | 內容 | 說明 |
|---|---|---|
| event_time | 事件發生的時間 | 來源自己記下的時間,UTC,到毫秒。保留期限與分區都依這一欄 |
| ingested_at | 寫進 ClickHouse 的時間 | 和 event_time 的差就是管線的延遲,正常約 5 秒 |
| correlation_id | 事件編號 | Vector 給每筆事件一個唯一編號。送往主機 A 的是同一個編號,od-bridge 轉送記錄裡的 cid 就是它 |
| source_system | 哪個元件看到的 | coraza(WAF)、suricata、crowdsec;vector 是安裝腳本送的測試事件 |
| event_class | 事件類別 | WAF 是 web_activity,Suricata 是 network_activity,CrowdSec 是 detection_finding |
| severity_id | 嚴重度 | 1 到 4,4 最高。由各來源自己的級距換算而來 |
| confidence | 信心分數 | 保留欄位,目前一律是 0 |
| actor_ip | 攻擊者位址 | 以 IPv6 型態存放,IPv4 位址顯示成 ::ffff:203.0.113.80 |
| actor_asn | 攻擊者所屬的自治系統編號 | 保留欄位,目前一律是 0 |
| actor_country | 攻擊者的國別 | 經 Cloudflare 進來的 WAF 事件有值,取自 Cloudflare 加上的標頭;其他情況多半是空的 |
| actor_ua | User-Agent | 只有 WAF 事件有 |
| actor_xff | X-Forwarded-For 標頭的原文 | 留作證據。判斷攻擊者是誰一律看 actor_ip |
| target_host | 被打的對象 | WAF 是請求的 Host 標頭,Suricata 是封包的目的位址,CrowdSec 是記錄裡的主機名稱 |
| target_url | 被打的路徑 | 只有 WAF 事件有 |
| target_service | 服務名稱 | WAF 是 waf-nginx,CrowdSec 是 ssh,Suricata 沒有 |
| finding_title | 規則的說明文字 | |
| finding_rule_id | 規則編號 | WAF 是 CRS 的規則號,Suricata 是 SID,CrowdSec 是情境名稱 |
| finding_rule_set | 規則集 | OWASP CRS、Suricata-ET、CrowdSec |
| raw | 整筆事件的 JSON | 是 Vector 轉換之後的樣子,不是來源的原始記錄 |
實際的一筆長這樣。這是對主機 B 的 WAF 送一個帶 SQL 注入字串的請求之後查到的,查詢時指定直式輸出,一個欄位一行:
Row 1:
──────
event_time: 2026-10-05 06:51:19.399
ingested_at: 2026-10-05 06:51:24.406
correlation_id: cf394c49-b2b3-43bb-a784-355b5d02ebbe
source_system: coraza
event_class: web_activity
severity_id: 4
confidence: 0
actor_ip: ::ffff:192.168.0.50
actor_asn: 0
actor_country:
actor_ua: curl/8.5.0
actor_xff:
target_host: 192.168.0.112:8080
target_url: /beakplatform/?id=1%27%20UNION%20SELECT%201--
target_service: waf-nginx
finding_title: Host header is a numeric IP address
finding_rule_id: 920350
finding_rule_set: OWASP CRS
這一筆有兩個地方值得停下來看。
第一,時間是 UTC。台灣時間要加 8 小時,或在查詢裡寫 toTimeZone(event_time, 'Asia/Taipei')。
第二,規則欄位寫的是「Host 標頭是數字位址」,不是 SQL 注入。這個請求在 WAF 那邊其實命中了五條規則:920350(Host 標頭是數字位址)、942100、942190、942270(三條都是 SQL 注入),以及 949110(異常分數超過門檻,真正讓 WAF 回 403 的那一條)。一個請求在 events 裡只有一列,規則欄位取的是 WAF 記錄裡排第一的那條。用 IP 位址連主機 B 的請求,排第一的幾乎都是 920350,所以依規則統計 WAF 事件時,這一條會佔掉大多數;主機 A 上的案件標題也是同一個值。這是安裝包目前的轉換方式造成的,不是 ClickHouse 的限制。要看一個請求命中的全部規則,得回主機 B 看 WAF 的 audit.log,而它只留 6 小時。
表上有六個欄位和主機 A 是共用的:來源、嚴重度、攻擊者位址、目標主機、規則編號、發生時間。主機 A 的路由、併案、時限與處置中心的清單讀的就是這一組,所以在 ClickHouse 查到的值,可以直接拿去對主機 A 上的案件。
ClickHouse 有、主機 A 沒有的,是下面這幾種:
| 事件 | 往主機 A 的那一路怎麼處理 |
|---|---|
| 內網位址打內網位址 | 全部丟掉。自家服務互連、內網設備彼此連線都屬於這一類 |
| Suricata 最低一級嚴重度的告警 | 全部丟掉 |
| 同一個來源重複命中同一條規則 | Suricata 每小時放 1 筆,WAF 每 5 分鐘放 5 筆,其他來源每 5 分鐘放 1 筆,超過的丟掉 |
| 所有來源加起來一分鐘超過 8 筆 | 超過的丟掉 |
這些限制是為了保護主機 A:每一筆送過去的事件都可能變成一張案件、啟動一條流程。掃描器一分鐘打 300 個請求,主機 A 不需要 300 張案件,只需要知道有這件事。但「這個來源到底打了幾次」「還有哪些位址也在打」這類問題,主機 A 上的數字就不能用了,那是至少幾筆,不是總共幾筆。要算總數,問 ClickHouse。
反過來,「全量」也有它的範圍。它指的是全部的告警,不是全部的流量:
兩個入口。在 --admin-ips 列出的電腦上,用瀏覽器開 http://192.168.0.112:8123/play,右上角填帳號 secstack 與密碼,就可以直接輸入 SQL。或者在主機 B 上用命令列:
#### 主機 B
cd /opt/integrated-waf
CH_PW=$(sudo grep '^CLICKHOUSE_PASSWORD=' .env | cut -d= -f2-)
sudo docker compose exec clickhouse clickhouse-client --user secstack --password "$CH_PW" -d secstack
下面四個查詢涵蓋了大部分的日常需要。
過去一天,各來源各幾筆。
SELECT source_system, count() AS n, max(event_time) AS latest
FROM events
WHERE event_time > now() - INTERVAL 1 DAY
GROUP BY source_system
ORDER BY n DESC;
某個位址留下了什麼。位址欄是 IPv6 型態,比對時用 toIPv6() 轉換,直接寫字串比不到。
SELECT event_time, source_system, finding_rule_id, finding_title
FROM events
WHERE actor_ip = toIPv6('203.0.113.80')
ORDER BY event_time;
1. │ 2026-10-04 19:34:12.113 │ crowdsec │ crowdsecurity/ssh-bf │ CrowdSec crowdsecurity/ssh-bf │
└─────────────────────────┴───────────────┴──────────────────────┴───────────────────────────────┘
把最新一筆的每個欄位都印出來。結尾的 FORMAT Vertical 就是第三節那種直式輸出,欄位多的時候比橫的好讀。
SELECT * EXCEPT raw
FROM events
ORDER BY ingested_at DESC
LIMIT 1
FORMAT Vertical;
過去一小時,每分鐘各來源幾筆。這個查的是彙總表。同一分鐘可能分成幾列存放,所以要用 sum() 加總。
SELECT minute, source_system, sum(events) AS n, sum(high_sev_events) AS high
FROM events_per_minute
WHERE minute > now() - INTERVAL 1 HOUR
GROUP BY minute, source_system
ORDER BY minute;
主機 A 的資安案件頁有一個「跨系統關聯」分頁。它拿案件的攻擊者位址回查 ClickHouse,把同一個來源在 WAF、Suricata、CrowdSec 各自留下的紀錄並排,補上第四節說的那段落差。這個分頁預設不會出現:主機 A 不知道 ClickHouse 在哪裡、密碼是什麼。安裝腳本沒有替讀者做這一步,因為它等於把主機 B 資料庫的密碼交給另一台主機。
要啟用,在主機 A 的 /opt/BeakPlatform/.env 加四行,然後重啟平台:
#### 主機 A
CLICKHOUSE_URL=http://192.168.0.112:8123
CLICKHOUSE_DB=secstack
CLICKHOUSE_USER=secstack
CLICKHOUSE_PASSWORD=(主機 B 的 .env 裡 CLICKHOUSE_PASSWORD 的值)
sudo systemctl restart beakplatform
主機 B 不必改,主機 A 的位址在安裝時就已經列進 ClickHouse 帳號的來源名單。設定之後,案件的攻擊者位址在 ClickHouse 查得到紀錄時,案件頁才會多出這個分頁;查不到、連不上或逾時,案件頁其餘部分照常。原系列與教材版提到這個分頁的地方,前提都是這一步已經做了。
| 限制 | 說明 |
|---|---|
| 不存封鎖決策 | 它只有偵測,沒有處置。誰在什麼時候封了哪個位址,紀錄在主機 A 的決策清單,以及主機 B 上 od-bridge 的 /decisions。舊版安裝包在 ClickHouse 建過一張準備放決策的表,但從來沒有程式寫入,2026-10-05 起的版本已經移除 |
| 不是存取記錄 | 只有命中規則的才會進來,第四節已說明 |
| WAF 事件只記一條規則 | 一個請求命中多條規則時,只留排第一的那條,第三節已說明 |
| raw 不是原始記錄 | 它是轉換之後的事件。來源的原始記錄留在主機 B 的檔案裡,WAF 的 audit.log 留 6 小時,Suricata 的 eve.json 留 48 小時 |
| 不參與任何判斷 | WAF 擋不擋、主機 A 建不建案、決策落不落地,都不經過它 |
| 不分企業 | 表裡沒有企業欄位。一個防禦節點配對一家企業,這一份就是那家企業的。主機 A 的案件頁回查之前,會先確認案件屬於登入者的企業,再拿該案件的位址去查 |
| 帳號不分讀寫 | 只有 secstack 一個帳號,能讀、能寫、能刪表。Vector 寫入、Grafana 讀取、人工查詢用的都是它。把密碼交給主機 A 或其他工具之前,要知道交出去的是管理權 |
| 連線不加密 | 8123 與 9000 都是明文。它假設主機 A、主機 B 與管理者的電腦在同一個可信任的網段 |
| 不會無限期保留 | 90 天之後自動刪除。實測在相同設定的表寫入 100 天前與 80 天前的資料各一筆,前者隨即被清除,後者留著 |
把它停掉會怎樣,實測過一次:停止 ClickHouse 容器 26 秒,這段期間送進 803 筆事件,其中 3 筆依規則該送主機 A。那 3 筆都在一秒內送到主機 A,沒有受到影響。ClickHouse 恢復之後,803 筆全部補寫進去,一筆沒少。寫不進去的事件是由 Vector 留在記憶體裡重試的,這份緩衝有上限、也不寫到磁碟;停得太久,或者期間 Vector 自己重啟,那段時間的全量就會有缺口,而主機 A 上的案件不受影響。
所以它在整體裡的位置是這樣的:擋人靠 WAF,判斷靠主機 A,ClickHouse 負責事後回答「當時到底發生了多少」。少了它,防護與處置照常,但主機 A 上每一個「至少幾筆」都找不到地方換成確實的數字。
容器狀態是 healthy,只代表它活著:
#### 主機 B
curl -s http://127.0.0.1:8123/ping
Ok.
要確認有東西寫進去,就送一個一定會被 WAF 擋下的請求,十秒後查最新一筆:
#### --admin-ips 列出的任一台電腦
curl -s -o /dev/null -w '%{http_code}\n' "http://192.168.0.112:8080/beakplatform/?id=1%27%20UNION%20SELECT%201--"
403
#### 主機 B,十秒後(CH_PW 的取法見第五節)
sudo docker compose exec clickhouse clickhouse-client --user secstack --password "$CH_PW" -d secstack \
--query "SELECT event_time, ingested_at, source_system, target_url FROM events ORDER BY ingested_at DESC LIMIT 1 FORMAT Vertical"
Row 1:
──────
event_time: 2026-10-05 06:51:19.399
ingested_at: 2026-10-05 06:51:24.406
source_system: coraza
target_url: /beakplatform/?id=1%27%20UNION%20SELECT%201--
查到剛才那個路徑,而且兩個時間相差約 5 秒,就代表 WAF 記錄、Vector 轉換、ClickHouse 寫入這一段是通的。這個請求來自內網、打的也是內網位址,依第四節的規則不會送主機 A,所以主機 A 上不會多一張案件。這正好是「ClickHouse 有、主機 A 沒有」的一個例子。
接著看寫入有沒有失敗過。ClickHouse 會記下每一次查詢與寫入:
SELECT type, count() AS n, sum(written_rows) AS written
FROM system.query_log
WHERE query_kind = 'Insert' AND event_time > now() - INTERVAL 1 DAY
GROUP BY type;
┌─type────────┬──n─┬─written─┐
1. │ QueryStart │ 63 │ 0 │
2. │ QueryFinish │ 63 │ 961 │
└─────────────┴────┴─────────┘
開始與完成的次數相同,而且沒有 Exception 開頭的類型,就是沒有失敗。同一張表也看得出誰在讀它:把條件換成 query_kind = 'Select' 並依 initial_address 分組,在沒有做第五節選用設定、也沒有自建 Grafana 面板的節點上,查詢的來源只會有管理者自己。
查不到剛才那一筆時,依序看三件事:WAF 有沒有回 403(回 200 代表請求沒被擋,也就沒有告警);sudo docker compose logs --since 5m vector | grep ch_events 有沒有重試的訊息,有的話是 Vector 寫不進 ClickHouse;都沒有的話,回頭確認 WAF 的 audit.log 有沒有記下這個請求(WAF 深入篇第 3 篇)。
在 2026-10-05 之前安裝的節點有一件事要處理。ClickHouse 出廠會把自己的診斷紀錄寫成幾張系統表,伺服器記錄也開在最詳細的等級,這些不會自動清除。實測一台節點運作 12 天,事件資料只有 132 KiB,診斷紀錄與伺服器記錄合計約 1.6 GB。新版安裝包把那幾張表關掉、把記錄降到只留警告,並移除既有的診斷表。更新方式是依序執行 sudo bash /opt/integrated-waf/install.sh --update 與 --reconfigure;事件資料不受影響。