ufw status 顯示 deny by default、規則清單乾乾淨淨;docker compose up 之後,資料庫的 5432 埠卻對整個網際網路開放。這不是 bug,也不是設定疏失——是 UFW 與 Docker 各自管理 iptables 的不同鏈,而容器流量走的那條鏈,UFW 從頭到尾沒有參與。這是我們在主機組態檢測中最常遇到、也最常被誤解的一種暴露。本文解釋機制、給出驗證方法,以及依需求分層的修正方式。
Linux 的封包過濾依封包去向走不同的鏈:目的地是主機自身的封包走 INPUT,轉發給其他目的地(包括容器)的封包走 FORWARD。
-p 或 compose 的 ports:)時,自行在 FORWARD 鏈與 NAT 表寫入自己的規則(DOCKER 鏈),把外部流量直接導向容器。結果是:容器的流量完全不經過 UFW 的 INPUT 規則。UFW 說「全部 deny」的同時,Docker 在另一條路上說「這個埠放行」,而後者先到。兩套工具都正常運作、都沒有錯誤訊息——它們只是互相不知道對方存在。
ports: 發布且綁定 0.0.0.0(未指定介面時的預設值)的容器埠都受影響。反過來說,真正受 UFW 保護的只有主機網路上的服務——以及刻意只綁 127.0.0.1 的容器埠。設定檔看起來對不對不重要,重要的是實際生效狀態。依序執行:
# 1. 哪些埠在監聽、綁在哪個位址(0.0.0.0 與 [::] 即為對外)
ss -ltnp
# 2. Docker 是否已在 FORWARD 路徑寫入放行規則
sudo iptables -L DOCKER -n -v
sudo iptables -t nat -L DOCKER -n -v
# 3. 最終裁判:從另一台主機(主機防火牆之外)實際連線
nc -vz -w 3 你的主機IP 5432
第三步是黃金標準。只有從外部實際連得上或連不上,才是事實;主機上的任何檢視都可能漏掉另一層轉譯。若埠在 IPv6 上也有監聽([::]:5432),外部驗證要 v4、v6 各測一次——我們看過只做了 IPv4 過濾、而 IPv6 那側完全裸露的環境:主機當下沒有全域 IPv6 位址所以「還沒被利用」,但只要供應商哪天啟用 IPv6,暴露立即成立,而且不會有任何通知。
這是最重要、也最常被略過的一步。資料庫、快取、只給反向代理存取的後端,都不需要發布到 0.0.0.0:
services:
db:
ports:
- "127.0.0.1:5432:5432" # 只有主機自己連得到
# 更好:若只有同一個 compose 網路內的容器需要連線,
# 整個 ports: 區塊都可以拿掉——容器間走內部網路,不需要發布埠。
發布埠的正確判準是「誰需要從主機外部連進來」,而不是「容器裡有什麼在監聽」。
真正需要對外、但要限制來源的埠,過濾規則要放在 DOCKER-USER 鏈。這是 Docker 官方保留給使用者的鏈:位於 FORWARD 最前端、在 Docker 自己的規則之前被評估,而且 Docker 重啟或重建規則時不會清掉它——這是它與「自己在 FORWARD 插規則」的關鍵差異,後者會在 Docker 服務重啟後被蓋掉,形成又一種靜默失效。
# 只允許特定網段存取容器發布的 5432
sudo iptables -I DOCKER-USER -p tcp --dport 5432 ! -s 203.0.113.0/24 -j DROP
網站掛在 Cloudflare 等 CDN 後方時,origin 的 80/443 理論上只該接受 CDN 的回源連線——否則攻擊者可以繞過 CDN 的防護直接打 origin。做法是把 CDN 公布的 IP 網段裝進 ipset,在 DOCKER-USER 只放行清單內的來源。三個實作要點,每一個都是實際踩過的:
對「已經綁 127.0.0.1、理論上不對外」的服務埠,在 UFW 額外放一條 deny 規則是值得的:它現在不做任何事,但防的是日後有人把 compose 設定改回 0.0.0.0 的情境——那種改動不會有人記得回來補防火牆。deny 規則沒有副作用,成本為零。
| 誤解 | 實際情況 |
|---|---|
| 「UFW 有開就是有保護」 | UFW 只保護 INPUT 路徑;容器發布埠走 FORWARD,UFW 不參與。 |
| 「我在 FORWARD 加了規則」 | Docker 重啟時會重建自己的規則,手插的規則順序可能被推到 Docker 規則之後而失效。使用者規則的正確位置只有 DOCKER-USER。 |
| 「設定檔沒問題就沒問題」 | 生效狀態由 iptables 的即時內容決定。驗證要看 iptables -L 與外部實測,不是設定檔。 |
| 「v4 過濾做完就結束了」 | docker-proxy 預設同時監聽 [::]。v6 側沒有對稱規則時,啟用 IPv6 的那一天就是暴露開始的那一天。 |
本文的檢查邏輯——資料庫埠發布到 0.0.0.0、Docker API 對外曝露、特權容器、Docker socket 掛載等——已實作在我們的免費組態審查的 docker-compose 檢查中,貼上您的 compose 檔即可比對;伺服器不保存您上傳的內容。誠實揭露其限制:靜態審查看的是設定檔,不是生效狀態——它抓得到「compose 寫了 0.0.0.0」,抓不到「iptables 實際長什麼樣子」。生效狀態的驗證請依本文第二節的三個指令,或由自助主機健檢在主機上實際採集。