防禦節點(主機 B)上除了 WAF,還裝了幾個別人寫的系統。這一系列每篇只談一個,不深入它的設定細節,只回答三件事:它是什麼、它在主機 A 與主機 B 之間佔哪一段、少了它整體會缺什麼。本篇是 CrowdSec。
先講結論。WAF 與 Suricata 都是一次看一筆:一個請求、一個封包,對得上規則就記一筆。CrowdSec 讀的是記錄檔,判斷單位是「同一個來源在一段時間內做了幾次」。本安裝包只讓它做兩件事。第一,讀主機 B 的 SSH 登入記錄、偵測暴力破解,偵測結果和 WAF、Suricata 的告警走同一條路送到主機 A 建案。第二,替平台存一份封鎖決策,留給日後接上的執行元件使用。它自己不擋任何連線,也不連 CrowdSec 官方的雲端服務。
CrowdSec 是法國 CrowdSec 公司維護的開源專案,授權 MIT。它常被拿來和 Fail2ban 比較:兩者都是讀記錄、數失敗次數、然後封鎖來源。差別在 CrowdSec 把「偵測」與「動手擋」拆成兩個程式,中間隔一個 API;偵測的那一半只產生清單,要不要擋、在哪裡擋,是另一個程式的事。本安裝包用的容器映像是 crowdsecurity/crowdsec:v1.6.4。
它由幾個部件組成,本安裝包只啟用其中一部分:
| 部件 | 做什麼 | 本安裝包的狀態 |
|---|---|---|
| 記錄來源(acquisition) | 指定要讀哪些記錄 | 只有一個:主機 B 的 /var/log/auth.log |
| 解析器(parser) | 把一行文字拆成欄位:時間、程式、來源位址、帳號 | syslog 與 sshd 的解析器 |
| 情境(scenario) | 依欄位計數,達到門檻就產生一筆告警 | 只有 SSH 相關的三組,沒有任何 HTTP 類情境 |
| LAPI(本機 API) | 存告警與決策,供其他程式讀寫 | 啟用,只開主機 B 本機的 8081 埠 |
| bouncer | 向 LAPI 取決策清單,真正動手擋 | 沒有安裝 |
| Central API | 官方的雲端服務:上傳偵測訊號、下載社群黑名單 | 關閉 |
| 通知外掛 | 情境命中時,把告警送到別的地方 | 啟用 http 外掛,送給同一台主機上的 Vector |
關掉 Central API 有兩個理由。開啟時,所有寫進 LAPI 的決策都可能被當成偵測訊號上傳,包含平台下發的封鎖目標(舊版安裝包實測如此)。而下載回來的社群黑名單,在沒有 bouncer 的節點上也沒有人使用。要開啟的話,把 docker-compose.yml 裡 crowdsec 的 DISABLE_ONLINE_API 改成 "false",再執行 --reconfigure。

圖上有三件事要看。
CrowdSec 的情境用的是漏桶(leaky bucket)。每個來源位址有一個自己的桶子,每出現一筆符合條件的記錄就倒進一滴,桶子同時以固定速度往外漏。倒得比漏得快,桶子滿出來,就是一筆告警。偶爾打錯一次密碼的人,那一滴很快就漏掉了;連續猜密碼的程式,幾秒內就會滿。
本安裝包載入的情境有三組、五個桶子:
| 情境 | 計算的對象 | 容量 | 漏的速度 |
|---|---|---|---|
| ssh-bf | 同一來源的登入失敗 | 5 | 每 10 秒一筆 |
| ssh-bf_user-enum | 同上,但只算不同的帳號名稱(試帳號,而不是試密碼) | 5 | 每 10 秒一筆 |
| ssh-slow-bf | 同一來源的登入失敗,放慢速度的版本 | 10 | 每 60 秒一筆 |
| ssh-slow-bf_user-enum | 同上,只算不同的帳號名稱 | 10 | 每 60 秒一筆 |
| ssh-cve-2024-6387 | 認證完成前就逾時或異常中斷的連線,這是利用 OpenSSH 該漏洞時會留下的痕跡 | 3 | 每 180 秒一筆 |
以 ssh-bf 為例:容量 5,所以同一個來源在短時間內的第 6 次失敗會讓桶子滿出來。滿出來之後,同一個來源一分鐘內不會再產生第二筆告警。
有一類來源永遠不進桶子。CrowdSec 的解析階段有一份內建的白名單,來源是本機回送位址(127.0.0.1、::1)或三段私有網段(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)的記錄會直接略過。也就是說,從內網連主機 B 而打錯密碼,不論幾次都不會告警。
把它和前兩篇的元件放在一起,可以看出它補的是哪一塊:
| 比較 | WAF | Suricata | CrowdSec |
|---|---|---|---|
| 看什麼 | 經它轉送的 HTTP 請求 | 實體網卡上的封包 | 記錄檔裡的一行行文字 |
| 判斷單位 | 一個請求 | 一個封包或一條連線 | 同一來源在一段時間內的次數 |
| 看得到 SSH 登入的成敗嗎 | 看不到,SSH 不經過它 | 看不到,登入過程是加密的,只看得到有連線 | 看得到,sshd 自己寫在記錄裡 |
| 反應 | 當場回 403 | 只寫一行告警 | 只寫告警與決策,不擋 |
| 停掉的後果 | 網站斷線 | 網站照常,少一路告警 | 網站照常,少一路告警;平台決策少一個落地點 |
補上的這一塊有兩個特徵。其一,單獨一筆登入失敗不是攻擊,沒有辦法為它寫一條特徵規則,重點在次數。其二,「失敗」這件事只有 sshd 自己知道,網路上看到的只是一條加密連線。這兩點加起來,使得這種行為只能由讀記錄、會數次數的元件來偵測。
| 步 | 在哪裡 | 做什麼 |
|---|---|---|
| 1 | 主機 B,sshd | 登入失敗,經 rsyslog 寫一行到 auth.log |
| 2 | 主機 B,CrowdSec | 解析那一行,倒進該來源的桶子。桶子滿出來時產生一筆告警(alert) |
| 3 | 主機 B,CrowdSec | 在 LAPI 記一筆 4 小時的 ban,來源欄位是 crowdsec。沒有 bouncer,這筆 ban 只是紀錄,不擋人 |
| 4 | 主機 B,CrowdSec → Vector | http 通知外掛把告警送給 Vector。接收口只開在容器網路內,主機上沒有這個埠 |
| 5 | 主機 B,Vector | 只留「CrowdSec 自己偵測出來」的告警,轉成與 WAF 事件相同的格式:來源系統記為 crowdsec,規則編號用情境名稱,攻擊者是來源位址,目標主機取記錄裡的主機名稱 |
| 6 | 主機 B,Vector | 封頂:同一個情境、同一個來源,5 分鐘只放 1 筆。之後再過所有來源共用的總量上限 |
| 7 | 主機 B → 主機 A | od-bridge 簽章後送進平台的事件受理端點 |
| 8 | 主機 A | 依路由規則建案,進處置流程。資安人員簽核「封鎖」之後才產生封鎖決策,由 od-bridge 拉回落地 |
第 5 步的嚴重度是安裝包給的固定值。CrowdSec 的告警沒有嚴重度欄位,而會走到這一步的,都是累積到門檻、CrowdSec 自己判定該封鎖的行為,所以一律記為 3;情境名稱帶有 cve 的(目前只有 ssh-cve-2024-6387)記為 4。在示範企業的路由下,嚴重度 3 以上的事件走「SOC 團隊版」流程,由資安人員決定要不要封。
整條線可以這樣讀:CrowdSec 說「這個來源在猜密碼」,主機 A 上的人決定「要不要封它」。第 3 步那筆 4 小時的 ban 是 CrowdSec 的預設行為,本安裝包保留它當作紀錄,但沒有讓它生效。真正的封鎖只來自平台的決策。主機 A 不知道 CrowdSec 的存在,它收到的是一筆來源系統為 crowdsec 的事件,處理方式與 WAF、Suricata 的事件完全相同。
主機 A 的事件受理金鑰有一份「允許的事件來源」清單。2026-10-05 之前配對的節點,清單裡沒有 crowdsec,事件會被平台拒收,主機 B 上 od-bridge 的記錄是 src=crowdsec status=403,其他來源不受影響。處理方式是以企業管理員登入主機 A,到「安全中心 / API Key」編輯名稱為「integrated-waf intake」的金鑰,在「資安事件接收」的來源清單加一行 crowdsec。之後新產生的開通字串已內含這一項。
原系列第 3 篇講過,主機 A 核可的封鎖決策由 od-bridge 拉回主機 B,落在三個執行點:nftables(主機 B 自己擋封包)、EDL(一份純文字清單,給邊界防火牆抓)、CrowdSec 的 LAPI。od-bridge 以一個獨立的身分登入 LAPI,把決策寫成一筆 ban,期限等於流程指定的封鎖時間;解封或到期時再把它刪掉。位址、網段、國別、自治系統編號四種目標都可以寫。
LAPI 這一份和 EDL 的差別在格式。EDL 是一行一個位址的文字檔,沒有期限、每次都要整份重抓,是為了只吃網址清單的防火牆設備設計的。LAPI 的每筆決策帶有期限,bouncer 可以只問「上次之後多了什麼、少了什麼」。所以要讓另一台 Linux 主機、另一台 nginx 跟著平台的決策擋人,接 CrowdSec 的 bouncer 是比抓 EDL 更合適的路(原系列第 6 篇有完整比較)。本安裝包沒有裝 bouncer,所以出廠時這一份只是清單。
於是 LAPI 裡會有兩種決策,性質完全不同:
| 比較 | CrowdSec 自己的偵測 | 平台下發的決策 |
|---|---|---|
| 來源欄位(Source) | crowdsec | od-bridge |
| 誰決定的 | 情境的桶子滿了就自動產生 | 主機 A 的案件流程,經過簽核或流程的自動判斷 |
| 期限 | 固定 4 小時 | 流程指定,示範的 SOC 團隊版是 1 小時 |
| 平台解封時會被移除嗎 | 不會,平台不知道它存在 | 會 |
| 會變成主機 A 上的新事件嗎 | 會,這就是第四節的偵測結果 | 不會。否則平台每下一道封鎖,就會再收到一筆事件、再開一張案件 |
實測可以看到兩者的差別。對同一個位址,先由 CrowdSec 偵測到,再由平台封鎖,LAPI 裡會有兩筆。cscli decisions list 對同一個位址只列一筆,並在表格下方註明省略了重複的:
+--------+-----------+-----------------+-----------+--------+---------+----+--------+------------+----------+
| ID | Source | Scope:Value | Reason | Action | Country | AS | Events | expiration | Alert ID |
+--------+-----------+-----------------+-----------+--------+---------+----+--------+------------+----------+
| 352439 | od-bridge | Ip:203.0.113.80 | od-bridge | ban | | | 1 | 59m52s | 147 |
+--------+-----------+-----------------+-----------+--------+---------+----+--------+------------+----------+
1 duplicated entries skipped
在主機 A 撤銷封鎖之後,平台寫的那筆消失,CrowdSec 自己的那筆還在,要等 4 小時到期:
+--------+----------+-----------------+----------------------+--------+---------+----+--------+------------+----------+
| ID | Source | Scope:Value | Reason | Action | Country | AS | Events | expiration | Alert ID |
+--------+----------+-----------------+----------------------+--------+---------+----+--------+------------+----------+
| 352438 | crowdsec | Ip:203.0.113.80 | crowdsecurity/ssh-bf | ban | | | 6 | 3h58m21s | 146 |
+--------+----------+-----------------+----------------------+--------+---------+----+--------+------------+----------+
打算自己接 bouncer 的讀者要先想清楚這一點。bouncer 拿到的是 LAPI 的全部決策,包含 CrowdSec 自己產生的那一種。那一種沒有經過平台的簽核,不會出現在平台的決策清單裡,平台的解封也移除不了它。接上 bouncer 的那一刻,它就從紀錄變成真正的封鎖。
| 限制 | 說明 |
|---|---|
| 不擋任何連線 | 沒有 bouncer。LAPI 裡的 ban 不論是誰寫的,在主機 B 上都不會讓任何封包被丟掉 |
| 不看 HTTP | 安裝包沒有把網站或 WAF 的存取記錄交給它,也沒有安裝 HTTP 類的情境。對網站登入頁的暴力破解不在它的偵測範圍(原系列第 5 篇把這一項列為已知缺口) |
| 不讀 Suricata 的告警 | 兩者各自把結果送給 Vector,彼此不相通。同一個位址在兩邊各留下什麼,要到主機 A 的案件頁才會並排 |
| 內網來源不告警 | 內建白名單略過私有網段,第三節已說明 |
| 走 tunnel 的節點上,這項偵測平常是安靜的 | Internet 訪客經 tunnel 只到得了 WAF,碰不到主機 B 的 22 埠;主機 B 若只有內網位址,連得到 22 埠的都是私有網段,又被白名單略過。只有主機 B 的 SSH 對外可達時(有公網位址,或防火牆把 22 埠轉進來),才會有真實的命中 |
| 主機沒有 auth.log 就沒有資料 | 它依賴 rsyslog 寫的這個檔案。Ubuntu 22.04 與 24.04 的伺服器版預設有;只用 journal、沒有 rsyslog 的系統,這項偵測不會有任何輸入,也不會報錯 |
| 不分辨記錄的真假 | 它讀的是文字。主機 B 上任何能呼叫 logger 的本機帳號,都能寫出一行看起來像 sshd 的記錄,第七節的驗證就是利用這一點。偽造的偵測只會變成一張案件,封不封仍由主機 A 的流程與保護清單把關 |
| 不連官方服務 | 不上傳偵測結果,不下載社群黑名單 |
「平常是安靜的」和上一篇 Suricata 的結論相同,理由也相同:走 tunnel 的架構把攻擊者擋在主機 B 的網卡之外。這項偵測守的是主機 B 自己的管理入口,那個入口哪天對外開了,它才開始有事可做。
容器狀態是 Up 不算證據。先看它有沒有在讀記錄:
#### 主機 B
cd /opt/integrated-waf
sudo docker compose exec crowdsec cscli metrics show acquisition
Acquisition Metrics:
+-----------------------------+------------+--------------+----------------+------------------------+-------------------+
| Source | Lines read | Lines parsed | Lines unparsed | Lines poured to bucket | Lines whitelisted |
+-----------------------------+------------+--------------+----------------+------------------------+-------------------+
| file:/var/log/host/auth.log | 72 | - | 72 | - | - |
+-----------------------------+------------+--------------+----------------+------------------------+-------------------+
有這一列、而且 Lines read 會增加,代表記錄來源接上了。Lines parsed 是空的不代表壞掉:sshd 的解析器只認登入失敗這一類的行,auth.log 裡的 sudo、cron 記錄都算在 unparsed。要看到完整的一圈,得讓它真的偵測到一次。內網來源會被白名單略過,所以用 logger 寫 8 筆假的登入失敗,來源用文件示範專用的保留網段 203.0.113.0/24,它不是真實的主機:
#### 主機 B
for n in 1 2 3 4 5 6 7 8; do
logger -p auth.info -t sshd --id=$((4500+n)) "Failed password for invalid user admin from 203.0.113.80 port $((40300+n)) ssh2"
done
sudo docker compose exec crowdsec cscli alerts list -l 1
sudo docker compose logs --since 1m od-bridge | grep forwarded
+-----+-----------------+----------------------+---------+----+-----------+-----------------------------------------+
| ID | value | reason | country | as | decisions | created_at |
+-----+-----------------+----------------------+---------+----+-----------+-----------------------------------------+
| 146 | Ip:203.0.113.80 | crowdsecurity/ssh-bf | | | ban:1 | 2026-10-04 19:34:12.113939534 +0000 UTC |
+-----+-----------------+----------------------+---------+----+-----------+-----------------------------------------+
od-bridge-1 | 2026-10-04 19:34:13,223 INFO ingest: forwarded cid=73b5c1ac-807c-4ca7-9c1b-950c91657f4d src=crowdsec status=200
第一段證明情境有載入、桶子會滿。第二段證明告警經 Vector 到了 od-bridge,而且主機 A 收下了(status=200)。從寫入記錄到主機 A 收下,約一秒。此時再看一次 acquisition,Lines parsed 會是 8。
接著到主機 A 的「資安案件處置中心」,會多一張案件:
| 欄位 | 內容 |
|---|---|
| 標題 | CrowdSec crowdsecurity/ssh-bf |
| 偵測來源 | CrowdSec |
| 嚴重度 | 3 |
| 攻擊者 IP | 203.0.113.80 |
| 規則 ID | crowdsecurity/ssh-bf |
| 處置流程 | 資安事件處置(SOC 團隊版) |
在這張案件上簽核「封鎖攻擊來源」,幾秒後回主機 B 看三個落地點,就會得到第五節的那兩段輸出:
#### 主機 B
sudo docker compose exec crowdsec cscli decisions list # Source 為 od-bridge 的那筆
sudo nft list set inet secstack blocklist # 203.0.113.80 timeout 1h
curl -s http://127.0.0.1:8500/edl # 203.0.113.80
看完把測試留下的東西清掉。平台那筆在主機 A 撤銷即可;CrowdSec 自己記的那筆要手動刪:
#### 主機 B
sudo docker compose exec crowdsec cscli decisions delete --ip 203.0.113.80
沒有告警時依序查三件事:主機 B 有沒有 /var/log/auth.log;acquisition 那張表有沒有這個來源;同一個位址是不是五分鐘內已經測過一次(第四節第 6 步的封頂會把第二筆丟掉,換一個位址再試)。有告警但主機 A 沒有案件,看 od-bridge 那一行的 status:403 是第四節提到的金鑰來源清單,其餘照原系列第 10 篇第四節的做法查去程。
在 2026-10-05 之前安裝的節點,CrowdSec 的設定有幾處沒有作用,要先更新安裝包:依序執行 sudo bash /opt/integrated-waf/install.sh --update 與 --reconfigure。