回程要證明的是:不該封的封不了、該封的在幾秒內真的落到 kernel 與清單、到期之後真的消失。前面的 TEST- 案件就是這一節的起點。
| # | 要證明的事 | 性質 | 通過的樣子 |
|---|---|---|---|
| R1 | 保護清單會擋下不該封的目標 | 反證 | 攻擊者是內網位址的案件,封鎖決策寫不進去:決策列表沒有那個位址的 block,主機 B 的 blocklist 也沒有 |
| R2 | 決策被拉走 | 正證 | 按下「封鎖攻擊來源」之後 5 秒內,od-bridge 的 log 出現 decision … action=block target=ip/203.0.113.42;平台上該筆由 pending 變 applied |
| R3 | 交接點寫的一方(nftables):kernel 裡真的有這個元素 | 正證 | nft get element 印出元素與剩餘時間,結束碼 0 |
| R4 | od-bridge 看到的與 kernel 一致 | 正證 | /state/nft 的 blocklist 有同一個位址與剩餘秒數;allowlist 是 ADMIN_IPS 加平台主機 |
| R5 | 交接點寫的一方(EDL):清單檔有這一行 | 正證+反證 | /edl 回 200、多一行 203.0.113.42。封鎖前先看一次:空清單也要回 200 而不是 404 |
| R6 | 到期或撤銷之後真的消失 | 反證 | nft get element 結束碼 1;/edl 回到空;平台上原決策是 expired 或 revoked,並多一筆 unblock |
R5 的反證要在封鎖之前先看:
#### 主機 B:封鎖前
sudo nft get element inet secstack blocklist '{ 203.0.113.42 }'; echo rc=$? # rc=1
curl -s -D - http://127.0.0.1:8500/edl # HTTP 200,內容為空
然後用資安人員 linda.hu 打開 TEST- 案件,填處置意見後按「封鎖攻擊來源」,回主機 B:
#### 主機 B:封鎖後
cd /opt/integrated-waf
sudo docker compose logs --tail 30 od-bridge | grep 'decision ' # R2
sudo nft get element inet secstack blocklist '{ 203.0.113.42 }'; echo rc=$? # R3:rc=0
curl -s http://127.0.0.1:8500/state/nft # R4
curl -s http://127.0.0.1:8500/edl # R5
odstat # processed 與 applied 各加一
table inet secstack {
set blocklist {
type ipv4_addr
flags interval,timeout
elements = { 203.0.113.42 timeout 1h expires 59m41s212ms }
}
}
rc=0
{"blocklist": {"203.0.113.42": 3581}, "blocklist6": {}, "allowlist": ["192.168.0.111", "192.168.0.50"]}
203.0.113.42
平台這一側用示範企業管理員看「開放防禦 → 決策列表」(這一頁只有企業管理員看得到):
R6 二選一:等 TTL 到期(標準流程是 1 小時),或由企業管理員在決策列表撤銷。之後把「封鎖前」那兩行指令再打一次,結果要回到 rc=1 與空清單。/edl 最晚會比 kernel 多留 60 秒,那是 od-bridge 清理過期項目的週期。
R1 的做法是送一筆「攻擊者是內網位址、目標是外部主機名稱」的合成事件(目標不是內網才過得了去程的過濾),讓它建案後照常處置:
#### 主機 B
inject TEST-PROT 192.168.0.77
#### 到平台處置「驗證事件 TEST-PROT」這張案件之後:
sudo nft get element inet secstack blocklist '{ 192.168.0.77 }'; echo rc=$? # 仍是 rc=1
出廠的保護清單涵蓋 RFC1918 等 16 個網段(第 5 篇第三節),任何流程要寫封鎖決策之前都會比對,命中就不寫。案件在畫面上怎麼收尾依流程而定(出廠流程會先判斷有沒有公網來源,沒有就轉給資安人員人工確認),但不管怎麼處置,決策列表都不會出現 192.168.0.77 的 block,主機 B 也不會有這個元素。這一項與 R3 是一對:203.0.113.42 封得了、192.168.0.77 封不了,兩個都成立才能說保護清單在運作。
R3 證明的是「清單裡有」,不是「攻擊者被擋了」。203.0.113.42 是文件保留位址,不會真的有封包從它來。要看 kernel 真的丟包,得封一台自己控制、又不在 allowlist 裡的機器(內網機器要先在保護清單加豁免,第 5 篇第三節),然後從那台連主機 B 的 22 或 8500 埠,結果應該是逾時。更重要的是效力範圍:走 Cloudflare Tunnel 的訪客不經過主機 B 的實體網卡,blocklist 對他們沒有作用(第 5 篇第二節、第 8 篇第一節)。回程六項全過,代表「決策會落地」,不代表「Internet 上的攻擊者被封了」;後者要看 EDL 有沒有邊界設備在抓。
前面三節是逐點驗證。另一種驗法是拿同一段時間的幾個數字互相對:有些應該相等,有些應該是大於,而且差距有固定的來源。差距的來源對不上,就是有東西壞了。
| # | 左邊 | 關係 | 右邊 | 差距的正當來源 |
|---|---|---|---|---|
| 1 | audit.log 的行數 | = | ClickHouse 的 WAF 事件數 | 每小時整點輪替的那幾秒可能少幾行(第 5 篇第七節)。持續少很多是 Vector 沒在讀 |
| 2 | ClickHouse 的事件數 | ≥ | od-bridge 的 total 增量 | 過濾(內網對內網、Suricata 資訊級)與三道封頂。左邊遠大於右邊是正常的;右邊長期是 0 而左邊有公網來源的事件,才是問題 |
| 3 | od-bridge 的 2xx 增量 | = | 平台 od_intake_events 的新增筆數 | 重複的 correlation_id 平台回 200 但不新增。4xx、5xx、unreachable 有任何成長都要追 |
| 4 | 平台的事件數 | ≥ | 新案件數 | 同攻擊者同規則 60 分鐘內併案(第 3 篇第三節) |
| 5 | 平台上生效中、執行點含 nftables 的 block 決策 | = | kernel blocklist 的元素;/edl 的行數(執行點含 edl 者) | 同一目標重複封鎖是多筆決策對一個元素;主機 B 重開機或有人手動重建表之後左邊會多於右邊,直到 TTL 到期(第 8 篇第四節)。平台的「對帳」欄做的就是這一列 |
第 3、4 列在主機 A 查,第 2 列在主機 B 查:
#### 主機 A:最近 24 小時平台收到幾筆事件、分屬幾張案件
sudo -u postgres psql -d beakplatform <<'SQL'
SELECT count(*) AS events, count(DISTINCT case_secure_code) AS cases
FROM od_intake_events
WHERE received_at > (now() AT TIME ZONE 'UTC') - interval '24 hours';
SQL
#### 主機 B:最近 24 小時 ClickHouse 裡攻擊者是公網位址的事件數(這些才有機會送平台)
sudo docker compose exec -T clickhouse clickhouse-client --user secstack --password "$CH_PW" --query \
"SELECT source_system, count() FROM secstack.events
WHERE event_time > now() - INTERVAL 24 HOUR
AND NOT (startsWith(IPv6NumToString(actor_ip),'::ffff:192.168.') OR startsWith(IPv6NumToString(actor_ip),'::ffff:10.') OR startsWith(IPv6NumToString(actor_ip),'::ffff:172.'))
GROUP BY source_system"
平台的時間欄位存的是不帶時區的 UTC,所以條件要寫 now() AT TIME ZONE 'UTC';直接拿 now() 去減,在非 UTC 時區的主機上會差出整個時區的小時數。
這是一台實際運作中的防禦節點上發生過的事,位址已去識別化。
現象。節點連續運作七天。所有容器都是 Up,有健康檢查的都是 healthy;SQL injection 探測回 403;決策輪詢每 5 秒一次、每次都回 200。看起來一切正常。但 odstat 是這樣的:
… Intake (Vector → bridge → BP) 25670 total 21 2xx 0 4xx 25649 5xx 0 unreachable …
兩萬五千多次轉送,成功的只有 21 次。翻 od-bridge 的 log,同一個 correlation_id 反覆出現:
INFO ingest: forwarded cid=3bf3afd5-… src=suricata status=500
INFO ingest: forwarded cid=3bf3afd5-… src=suricata status=500
INFO ingest: forwarded cid=3bf3afd5-… src=suricata status=500
原因是兩件事疊在一起。平台對某一筆事件固定回 500;od-bridge 把 5xx 轉成 502 回給 Vector,而 Vector 的 http sink 對 5xx 會重送同一筆,直到成功為止。這一筆永遠不會成功,於是它佔住了送往平台的那一路,排在它後面的事件一筆都送不出去。從那一刻起的四天半,線外去程是停的。
| 當時看得到的證據 | 結果 | 為什麼沒有幫助 |
|---|---|---|
| 容器狀態 | 全部正常 | 沒有任何行程掛掉。Vector、od-bridge、平台都活著,只是在重複同一件失敗的事 |
| SQL injection 探測 | 403 | 線上的證據。線上與線外之間沒有呼叫關係,線外停了,線上不會知道 |
| ClickHouse | 事件持續寫入 | 全量那一路與送平台那一路是分開的,一路卡住不影響另一路 |
| 決策輪詢 | 每 5 秒回 200 | 回程的證據。去程與回程是 od-bridge 裡兩個獨立的迴圈 |
| 平台的案件處置中心 | 很安靜 | 「沒有新案件」與「沒有人在打」長得一模一樣 |
抓到它的是第四節 O4 的那一行,以及第六節對帳表的第 2、3 列:這段期間 ClickHouse 有來自公網位址的 Suricata 事件,平台卻一筆都沒收到,5xx 則一直在長。這就是「分線驗」的意思。線上的六項、回程的六項在這個狀態下全部通過,只有去程的檢查會失敗。
去程的健康訊號就這三個,都在 odstat 那一行裡。5xx 與 unreachable 兩次查看之間沒有成長;4xx 沒有成長(401 是開通字串失效,422 是沒有事件路由,403 是金鑰的來源系統範圍不含這個來源);od-bridge 的 log 裡沒有同一個 cid 連續出現。回程的訊號是另外兩個:last executor iter 在 10 秒以內,current token valid for 大於 0。
| 你看到的 | 它證明 | 它不證明 |
|---|---|---|
| 容器全部 Up/healthy | 行程活著、健康端點有回應 | 任何一條線在做它該做的事 |
| --verify 通過 | 線上在擋(L3)、元件活著、連得到平台 | 事件有送達平台、決策會落地。它不送任何事件 |
| SQL injection 回 403 | 線上 | 線外任何一段;也不證明正常請求過得去(要配 L2) |
| --test-event 建出案件 | 去程從 Vector 注入口到平台建案這一段 | WAF 在擋、Vector 有在讀 audit.log。合成事件從 8688 進去,不經過 WAF 也不經過檔案,所以它要配 O1 |
| ClickHouse 有事件 | Vector 讀到並解析了 | 平台收到了。那是另一路 |
| 平台沒有新案件 | 沒有事件通過過濾與封頂,或者去程壞了 | 沒有被攻擊。要分辨是哪一種,看 ClickHouse 與 odstat |
| 決策狀態 applied | od-bridge 當時回報執行成功 | kernel 現在還有那個元素(重開機、重建表之後就沒有);也不證明攻擊者被擋了(看效力範圍) |
| blocklist 是空的 | 目前沒有生效中的封鎖 | od-bridge 壞了。TTL 到期、重開機、流程只選了 EDL 執行點都會是空的 |
| /edl 有那個位址 | 清單檔寫好了 | 有設備在抓、抓了有套用。安裝包內沒有任何元件讀 EDL(第 6 篇) |
| 處置中心的案件數 | 有幾個「攻擊者 × 手法」被送上來 | 被打了幾次。次數要問 ClickHouse(第 5 篇第五節) |
| 時機 | 至少跑這樣 |
|---|---|
| 剛裝完 | 十八項全部跑一次。順序照本篇:線上、去程、回程 |
| 改了主機 B 的 .env 並 --reconfigure | L1 到 L3(來源管制與 WAF 還在)、R4(allowlist 是新的名單、生效中的封鎖有補回來)、odstat |
| 改了 WAF 覆寫檔、升級 WAF 映像 | L2 到 L6。新版 CRS 可能多出規則,L2 與 L5 這兩個反證最容易在這時候失敗 |
| 改了平台的事件路由、流程、保護清單 | O4、O5、R1 到 R3 |
| 重發開通字串、平台重裝或換位址 | O4(事件受理金鑰)、R2(執行帳號)。兩把憑證是分開的,要各驗一次 |
| 主機 B 重開機 | L1 到 L3、R4。blocklist 會歸零,這是預期(第 8 篇第六節),對帳表第 5 列會暫時不相等 |
| 覺得平台太安靜 | 第六節對帳表的第 2、3 列 |
日常只要三行,各對一條線:
#### 1. 線上(工作機):預期 403
curl -s -o /dev/null -w '%{http_code}\n' "http://192.168.0.112:8080/beakplatform/?id=1%27%20OR%201=1--"
#### 2. 線外的兩個迴圈(主機 B):5xx、unreachable 沒有成長;last executor iter 在 10 秒內;token 大於 0
odstat
#### 3. 去程有沒有貨(主機 A):ClickHouse 有公網來源事件的日子,這裡不該是 0
sudo -u postgres psql -d beakplatform -Atc "SELECT count(*) FROM od_intake_events WHERE received_at > (now() AT TIME ZONE 'UTC') - interval '24 hours'"
三行分別回答三個問題:線上還在擋嗎、線外的去程與回程還在轉嗎、轉的時候有沒有東西真的送到。任何一行不對,回到對應那一節逐項查;三行都對,就是第 9 篇那張圖上的兩條線各自在做它該做的事。