安裝包裡有六套開源元件,但 EDL 不是任何一套產出的,它是自建的 od-bridge 產出的。這篇把 EDL 的來源、落地路徑、對外 URL、以及「誰該讀它、誰不該讀它」一次講清楚。特別要說的是:不建議拿 Linux 的軟體防火牆去吃這份 EDL,理由在第六節。
先講結論。EDL 是 od-bridge 把平台核可的封鎖決策「翻譯成純文字檔」再用 HTTP 供應出去的產物,格式是一行一個 IP,設計給企業既有的邊界防火牆(Palo Alto、FortiGate 這類支援 External Dynamic List 的設備)定期抓取。安裝包內的六套開源元件沒有任何一套讀它;防禦節點自己的封鎖靠的是 nftables 與 CrowdSec,那兩條是 od-bridge 直接寫入的,不經過 EDL 檔。沒有專業設備的環境,EDL 產出來就只是擺著,這是預期狀態,不是故障。另外平台本身還自產一份只含黑名單的 EDL(第四節),給沒有防禦節點的部署用。
EDL 是 External Dynamic List 的縮寫,來自 PAN-OS 的用語。概念很單純:防火牆上設一條規則,來源不寫死 IP,而是指向一個 URL;防火牆依排程重新抓那個 URL,抓到的內容就是當下的清單。它有三個特性,後面的設計都是從這三點推出來的:
| 特性 | 對設計的影響 |
|---|---|
| 格式極簡,一行一個 IP 或 CIDR | 沒有欄位可以放到期時間。清單裡的每個項目「什麼時候該消失」只能由產出端管,設備端不知道 |
| 抓到什麼就是什麼 | 每次抓取都是全量替換,不是增量。所以產出端必須每次重繪整份檔案,不能只在檔尾追加 |
| 只能從 URL 取得 | 檔案寫在磁碟上沒有用,得有一個 HTTP 服務把它送出去,而且回應格式要是設備認得的純文字 |
第 3 篇講過,事件送上平台建案、流程核可封鎖之後,決策是由 od-bridge 拉回防禦節點的(每 5 秒輪詢一次 /api/open_defense/decisions)。每筆決策帶一個 enforcement_points 欄位,寫明這筆決策要落在哪些執行點;od-bridge 拿自己 .env 裡的 MY_ENFORCEMENT_POINTS(預設 nftables,crowdsec,edl)與它取交集,交集內的每個執行點各跑一個 enforcer。EDL 是三個 enforcer 之一,與 nftables、CrowdSec 平行,彼此不相依。
edl enforcer 的實作重點只有一個:它是 reconciler,不是 appender。理由回到第一節的第一個特性:EDL 格式沒有 TTL 欄位,追加式寫法永遠刪不掉到期項目,清單只會長到設備拒絕載入。所以 od-bridge 把權威集合放在 state.json(每筆自帶 expires_at),每次收到決策或每 60 秒的 prune 都重繪整份 blocklist.txt 與 allowlist.txt。新增會累積,到期會自己掉出去。寫檔走 temp file 加 os.replace,設備抓到一半不會讀到半截檔。
| 平台決策 action | 寫進哪份清單 | 行為 |
|---|---|---|
| block | blocklist.txt | 加入或更新該 target 的到期時間(ttl_seconds 為空即永久) |
| allow | allowlist.txt | 同上;設備端要把 allow 規則放在 deny 規則之上才有意義 |
| unblock | 兩份都檢查 | 從持有它的清單移除,已不在也視為成功(冪等) |
| target 不是 ip / cidr | — | 回 unsupported_target_type,EDL 只認位址 |
| 項目 | 值(以本系列的主機 B 192.168.0.112 為例) |
|---|---|
| 安裝目錄 | /opt/integrated-waf |
| EDL 目錄(主機 OS) | /opt/integrated-waf/od-bridge/state/edl/ |
| 黑名單檔 | .../edl/blocklist.txt |
| 白名單檔 | .../edl/allowlist.txt |
| 權威狀態檔 | .../edl/state.json(含每筆的 expires_at、severity、reason、決策與案件 secure_code) |
| 容器內對應路徑 | /state/edl,由 compose 的 ./od-bridge/state:/state 掛入;.env 的 EDL_DIR=/state/edl |
| 檔案擁有者與權限 | root,0600。查看要 sudo cat |
| 黑名單 URL | http://192.168.0.112:8500/edl |
| 白名單 URL | http://192.168.0.112:8500/edl/allow |
| 供應者 | od-bridge 容器內的 aiohttp(INGEST_LISTEN=0.0.0.0:8500),因 network_mode: host 直接綁在主機埠上,與 /events、/stats、/decisions 同一個服務 |
| 回應格式 | text/plain; charset=utf-8,一行一個位址,排序後輸出(兩次抓取之間可 diff) |
| EDL_HEADER | 預設 0。設 1 會在檔頭加 # 註解行,方便人用 curl 看,但 PAN-OS 只文件化 [address][space][comment] 格式,獨立的 # 行設備可以拒收。給設備抓的環境一律維持 0 |
| EDL_PRUNE_INTERVAL | 預設 60 秒。沒有新決策時,到期項目最晚 60 秒後從檔案消失 |
| 8500 埠的來源限制 | nftables input chain 只放行 ADMIN_IPS 與平台 IP,其餘 drop。設備 IP 不在清單內的症狀是連線逾時,不是 403 |
空清單回 200 而不是 404,是刻意的。還沒有任何封鎖時 /edl 回一個空的 200。若回 404,PAN-OS 會沿用上一次抓到的內容,等於把一份過期的黑名單靜默釘在設備上。所以 curl 看到「200 但沒內容」是正常狀態,代表目前沒有生效中的封鎖。
前三節講的都是主機 B 上 od-bridge 那份。平台本身還有一份:主機 A 每分鐘用排程把該企業所有有效的封鎖決策渲染成一個 blocklist.txt,同時可從檔案分享與 HTTP 取得。兩份的內容本來就不同,看到不一致不要當成同步失敗。
| 項目 | 主機 B od-bridge 那份(第二、三節) | 主機 A 平台端那份 |
|---|---|---|
| 檔案位置 | /opt/integrated-waf/od-bridge/state/edl/blocklist.txt | <OD_EDL_OUTPUT_DIR>/<企業 secure_code>/blocklist.txt,未設定時是安裝目錄下的 data/edl;每個企業各一份 |
| URL | http://192.168.0.112:8500/edl | http://192.168.0.111:8000/beakplatform/edl/<企業 secure_code>/blocklist.txt |
| 收哪些決策 | 只收 enforcement_points 標了 edl 的決策,而且只有這台節點拉得到的(受 SA 的授權範圍限制) | 該企業所有有效的 block,不看 enforcement_points |
| 清單 | 黑名單+白名單兩份,各自獨立 | 只有黑名單。allow 與 unblock 都當 override:決策時間晚於某筆 block 的 override 會把那筆 block 排除,之後再來新的 block 會重新出現 |
| 更新時機 | 決策拉到即重繪,另有每 60 秒的 prune | cron 每分鐘一次(scripts/cron/od_render_edl.py),寫入走 temp file 加 rename |
| 過期判定 | state.json 內每筆的 expires_at | 決策的 expires_at,刻意不看 status:只接 EDL、沒有其他執行端的部署裡,決策會一直停在 pending,看 status 的話過期項目永遠掉不出去 |
| 來源限制 | ADMIN_IPS,nftables 層 drop,症狀是逾時 | 平台 .env 的 OD_EDL_ALLOWED_IPS,未設定=一律拒絕;另有每分鐘 60 次的流量限制 |
| 拒絕時的回應 | 不在名單內連不上 | 來源不在名單、企業不存在、檔案還沒產生,一律 404 且不區分原因,避免對外洩漏哪個企業存在 |
| 故障時的行為 | 沒有封鎖時回空的 200(第三節的說明) | fail-closed=不寫檔,保留舊檔。空清單在防火牆語意上等於解除全部封鎖,一次資料庫故障就清空防線,所以任何例外都不覆蓋既有檔案 |
| 取得方式 | 只有 HTTP | HTTP,或把 OD_EDL_OUTPUT_DIR 指到既有的 Samba/NFS 分享目錄,防火牆或管理者從網路磁碟讀同一個 txt |
為什麼要有第二份:沒有防禦節點的部署也能接防火牆。只裝主機 A、不裝主機 B 的環境,決策沒有任何執行點會落地,平台端這份就是它唯一的出口;有主機 B 的環境,它則是不依賴 od-bridge 存活的備援來源。
要接哪一份,看你要的語意。需要「放行清單壓過其他封鎖來源」(第 7 篇第三節)、或設備已經在抓主機 B 的其他服務,接 od-bridge 那份;沒有主機 B、想從網路磁碟讀檔、或只想要一份「已經算好誰該封」的黑名單,接平台端那份。不要兩份都接進同一台設備:同一個 IP 在兩邊的答案可能不同(第 7 篇第四節),設備會依規則順序取其一,排錯時很難看出是哪份說了算。
三個 enforcer 平行,但消費者完全不同。這張表是判斷「我的環境該接哪一條」的依據:
| 執行點 | od-bridge 怎麼落地 | 誰在消費 | 擋在哪 |
|---|---|---|---|
| nftables | 直接下 nft 命令進主機的 blocklist set(帶 timeout) | 主機 B 的 kernel,立即生效 | 主機 B 自己的 input hook。保護的是防禦節點本機(SSH、8500、host network 服務),詳見第 5 篇第二節 |
| CrowdSec | 經 LAPI 寫入 decision(帶 duration) | 任何接了 CrowdSec bouncer 的主機 | 裝 bouncer 的那台。官方有 nftables / iptables 的 firewall-bouncer,也有 nginx、Cloudflare 等 bouncer |
| EDL | 重繪純文字檔,HTTP 供應 | 安裝包外支援 External Dynamic List 的邊界設備 | 那台設備所在的網路邊界。安裝包內沒有任何元件讀它 |
EDL 的正確消費者是「已經站在攻擊者與網站之間」的邊界設備。典型是企業既有的 Palo Alto(EDL)、FortiGate(External Resource 的 IP address 類型)、或其他能訂閱 URL 清單的次世代防火牆。設備上只要做兩件事:新增指向 http://192.168.0.112:8500/edl 的外部清單、以它為來源建一條 deny 規則;再把設備的抓取來源 IP 加進主機 B 的 ADMIN_IPS。這是「防禦節點以外還有第二道邊界」以及「多台防禦節點想共享一份黑名單」這兩種場景的解法。
沒有專業設備的讀者第一個想到的通常是:那我在某台 Linux 上寫個 cron,定期 curl 這個 URL 塞進 iptables 或 nftables,不就有邊界防火牆了?三種常見的放法都不建議,理由各不相同。
| 放法 | 建議 | 理由 |
|---|---|---|
| 主機 B(防禦節點)自己的 nftables / ufw 讀自己的 EDL | 不要 | 純粹重複。od-bridge 的 nftables enforcer 已經把同一筆決策直接寫進主機 B 的 blocklist set,而且帶 timeout。再讓它去抓自己的 EDL,是把同一份資料走兩條路寫進同一個 kernel,多一條會出錯的路、不多任何保護 |
| 另一台 Linux 用 cron 抓 EDL,塞進 ipset / nftables set | 不要 | EDL 格式沒有 TTL,所以那支 cron 必須每次全量替換整個 set,等於自己重寫一個劣化版的 od-bridge nftables enforcer。失敗模式都很難看:cron 停了,清單永久卡在最後一版;抓不到 URL 時要決定是清空還是沿用,兩種都不對;抓到一半換檔要處理原子性。而這台 Linux 若要吃平台決策,正確做法是裝 CrowdSec 的 firewall-bouncer:它從 LAPI 拿的決策自帶 duration、增量同步、官方維護,od-bridge 已經把決策寫進 LAPI 了,不必經過 EDL 這個中間格式 |
| Proxmox 宿主機(pve-firewall) | 不要 | pve-firewall 不支援訂閱 URL 清單,一樣得自建 cron 塞 ipset 或 alias,前一列的問題全部照樣存在。更根本的是位置不對:在 tunnel 部署下,流量從 cloudflared 出來時已經在主機 B 內部,宿主機的防火牆看不到攻擊者的真實 IP,抓到清單也沒有東西可以比對 |
不建議的核心理由只有一個:EDL 是為「只能吃 URL 清單」的設備設計的最低共同格式,它把 TTL、增量、原子性全部丟給產出端。Linux 主機從來不受這個限制,它有 CrowdSec bouncer 這條帶 TTL 與增量的正規路徑,也有 od-bridge 直接寫 nftables 的實作可以參考。拿 Linux 去吃 EDL,是主動放棄較好的介面去遷就較差的格式。
那沒有專業設備的讀者怎麼辦?誠實的答案是:在純 tunnel 部署下,EDL 沒有適合的落點,nftables 就是這個架構裡唯一有效的邊界(第 5 篇第二節的表格已經講過它擋得住什麼)。若手邊有 pfSense 或 OPNsense 這類自帶 URL 清單 alias 功能的防火牆發行版當網路邊界,那是可以接 EDL 的(它們的 URL table alias 本身就處理排程抓取與全量替換),但這已經是「有邊界設備」的場景,不是 Linux 軟體防火牆。本系列沒有實測這兩者,設定方式請以其官方文件為準。
在 ADMIN_IPS 內的任一台主機執行(不在清單內會逾時,這不是 EDL 壞了,是第三節最後一列,平台端那份則看 OD_EDL_ALLOWED_IPS):
# 1. 現況:預期 200,內容為空或一串 IP
curl -s -D - http://192.168.0.112:8500/edl
# 2. 送一筆測試事件,讓平台建案並產生封鎖決策(在主機 B 上)
sudo bash /opt/integrated-waf/install.sh --test-event
# 3. 平台核可封鎖後最多 5 秒,測試 IP 應出現在清單裡
curl -s http://192.168.0.112:8500/edl
# 4. 對照主機端檔案與權威狀態(含到期時間)
sudo cat /opt/integrated-waf/od-bridge/state/edl/blocklist.txt
sudo cat /opt/integrated-waf/od-bridge/state/edl/state.json
# 5. 等 TTL 到期後再抓一次,該 IP 應消失(最晚多等 60 秒的 prune 週期)
第 3 步與第 4 步的內容應一致,因為 /edl 就是原樣讀 blocklist.txt 回出去。若檔案有內容而 URL 是空的,先查 od-bridge 容器有沒有在跑(docker ps 找 od-bridge);若 URL 逾時,查 .env 的 ADMIN_IPS。
| 誤解 | 實際 |
|---|---|
| EDL 是 Suricata 或 CrowdSec 產出的 | 都不是。六套開源元件沒有一套產出或讀取 EDL,它由自建的 od-bridge 的 edl enforcer 產出、由同一個 od-bridge 的 HTTP 服務供應 |
| 外網進來第一關是 Suricata | Suricata 以 -i 旁路監聽網卡,是被動 IDS,不在流量路徑上、不擋也不轉送。實際順序是 nftables(input hook 的 blocklist)→ WAF(nginx + ModSecurity)→ 後端網站;Suricata 與 ModSecurity 只是偵測來源,事件經 Vector 送 od-bridge 再上平台 |
| 接了 EDL 就等於封鎖生效 | 只在那台設備真的站在攻擊者與網站之間才生效。純 tunnel 部署下攻擊流量從 Cloudflare 進來,不經過任何內網邊界,EDL 沒有落點 |
| 清單抓下來是空的,代表 EDL 壞了 | 空的 200 是「目前沒有生效中的封鎖」。要看有沒有壞,用 --test-event 造一筆決策再抓 |
| 主機 A 與主機 B 的兩份 EDL 內容不一樣,同步壞了 | 本來就不同(第四節的表):一份收所有 block 只出黑名單,一份只收標了 edl 的決策且黑白分開。它們之間沒有同步機制 |
| 把 EDL 檔複製到別處用 | 抓到什麼就是什麼,複製出去的那一份沒有 TTL、永遠不會更新。要用就訂閱 URL,不要複製檔案 |