iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
自我挑戰組

一鍵完成六套開源防禦系統整合系列 第 22 篇

ClickHouse:留在主機 B 的那一份全量

  • 分享至 

  • xImage
  •  

防禦節點(主機 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 的兩個埠沒有經過那一層,限制是寫在帳號上的。不在名單上的位址連得到埠,但登入會被拒絕。

二、它接在哪裡

https://ithelp.ithome.com.tw/upload/images/20261006/201842612A9aRVM7Cs.png

圖上有三件事要看。

  • 分岔點在轉格式之後、篩選之前。三個來源的告警先由 Vector 轉成同一種格式,接著分兩路。往 ClickHouse 的那一路不再做任何處理;往主機 A 的那一路還要過濾、封頂。兩路拿到的是同一筆事件、同一個編號,差別只在後者有可能被丟掉。
  • 寫入者只有 Vector。Vector 每 5 秒或累積滿 500 筆寫一批,所以一筆告警從發生到查得到,大約是 5 秒。WAF、Suricata、CrowdSec、od-bridge 都不直接碰 ClickHouse。
  • 讀取的入口有三個,出廠只通一個。網頁查詢介面與命令列隨時可用。Grafana 已經設好連線,但安裝包沒有附任何儀表板,面板要自己建,留到 Grafana 那一篇。主機 A 的案件頁可以回查,但主機 A 預設不知道 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。

反過來,「全量」也有它的範圍。它指的是全部的告警,不是全部的流量:

  • 沒有命中任何規則的正常請求不在裡面。它不是存取記錄。
  • Suricata 的連線、DNS、TLS 記錄不在裡面,那些只留在主機 B 的檔案裡 48 小時(搭配元件篇 1 第四節)。
  • CrowdSec 裡由平台寫入的封鎖決策不在裡面,只有 CrowdSec 自己偵測出來的才算告警。

五、怎麼查

兩個入口。在 --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 的案件頁回查

主機 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;事件資料不受影響。


上一篇
CrowdSec:會數次數的偵測,與一份有期限的封鎖清單
下一篇
cloudflared:由主機 B 往外撥出的對外入口
系列文
一鍵完成六套開源防禦系統整合 共 23 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言