SSD Nodes Learn
指南 Matt Connor作者: Matt Connor · 已更新 2026-07-24

Docker 繞過 UFW 防火牆如何解決?

當您在 UFW 設定 deny 規則卻發現 Docker 連接埠仍對外開放時,這是因為 Docker 會在 iptables 的 PREROUTING 鏈中直接修改 DNAT 規則。本文深入解釋為何 UFW 規則會被跳過,並提供兩種有效的解決方案,幫助您正確管理容器安全性。

Why Docker bypasses UFW

Docker 會繞過 UFW,是因為已發佈的容器連接埠不會經過 UFW 管理的防火牆規則。當您執行 docker run -p 8080:80 時,Docker 會在核心的 nat 表格中,於 PREROUTING 鏈寫入一條 DNAT (destination network address translation) 規則。該規則會在核心決定封包去向之前,將每個封包的目標地址重寫為容器的私有地址。接著,重寫後的封包會透過 Docker 控制的 FORWARD 鏈轉發至容器內。UFW 的規則位於 INPUT 鏈,封包永遠不會進入該鏈。因此,ufw status 顯示預設拒絕,sudo ufw deny 8080 回報成功,但 8080 連接埠仍對整個網際網路開放。

這並非 Docker 的錯誤,UFW 也沒有損壞。這兩項工具皆是在對同一個核心防火牆進行編程。Docker 的規則是在封包路徑的較早階段執行,因此不會觸發 UFW。本指南將演示此繞過現象、解釋其機制,並提供兩種有效的解決方案:在 127.0.0.1 上發佈連接埠,以及在 DOCKER-USER 鏈進行過濾。如果您尚未接觸過 UFW,請先參考 UFW 防火牆基礎指南 進行設定,因為預設拒絕的防火牆仍是伺服器其他功能的正確基礎。

在您自己的伺服器上觀察繞過現象

從一台啟動 UFW 且預設拒絕所有傳入流量的 VPS 開始。執行一個已公開連接埠的 Web 容器:

sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpine

ufw status verbose 顯示 Default: deny (incoming), allow (outgoing) 且沒有針對 port 8080 的規則。根據防火牆的報告,該連接埠是關閉的。現在請從另一台機器(而非伺服器本身)進行測試:

curl -I http://your-vps-ip:8080/
HTTP/1.1 200 OK

容器有回應。新增一條明確的拒絕 (deny) 規則並再次測試:

sudo ufw deny 8080/tcp

連接埠仍有回應,因為該拒絕規則位於封包永遠不會經過的 chain 中。UFW 並未失效,而是根本沒有被調用。這也是問題隱蔽的原因:系統不會在任何地方印出錯誤,部署運作正常,且防火牆狀態輸出看起來完全像是一台運作正常的鎖定伺服器。

機制:PREROUTING 的執行順序早於 INPUT

核心依據固定順序處理傳入的封包,問題的核心就在於此順序。

  1. PREROUTING 最先執行。此處的規則可能會重寫封包的目標位址,Docker 對已發布連接埠(published port)的規則正是如此。
  2. 接著進行路由決策。目標為主機本身的封包會進入 INPUT 鏈。目標為其他機器的封包則進入 FORWARD 鏈。
  3. UFW 的規則位於 INPUT。Docker 的規則位於 FORWARD

查看您剛啟動的容器所對應的 Docker 規則:

sudo iptables -t nat -L DOCKER -n
Chain DOCKER (2 references)
target   prot opt source       destination
RETURN   0    --  0.0.0.0/0    0.0.0.0/0
DNAT     6    --  0.0.0.0/0    0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80

DNAT 行是問題的關鍵。任何傳向 port 8080 的封包,其目標位址都會被重寫為 172.17.0.2:80,即該容器在 Docker 私有 bridge 網路上的位址。重寫後,封包的目標不再是主機,因此路由決策會將其導向 FORWARD 路徑,Docker 已在此路徑中加入規則以允許進入其網路的流量。您的 deny 8080/tcp 規則則在 INPUT 等待一個永遠不會抵達的封包。

在 Ubuntu 24.04 上,iptables 指令是 nftables 的前端介面,但鏈的順序與結果完全相同。UFW 與 Docker 皆寫入同一個核心封包管線,且 Docker 的進入點較早。

常見的解決方案:將連接埠發布至 127.0.0.1

大多數容器原本就不需要對外公開。例如資料庫、位於反向代理後的應用程式伺服器、管理介面或指標端點:這些服務都不應直接回應來自網際網路的請求。請將它們發布至 loopback 位址:

docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpine

或是於 Compose 檔案中設定:

services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "127.0.0.1:8080:80"

此方法有效,是因為 Docker 的 DNAT 規則現在僅會比對目標位址為 127.0.0.1 的封包。由於來自網際網路的封包不可能擁有該目的地位址,因此核心會在執行任何防火牆規則前將其丟棄。該連接埠僅能從 host 端存取,無法從其他地方存取。請驗證綁定狀態:

sudo ss -tlnp | grep 8080

輸出結果應顯示 127.0.0.1:8080,而非 0.0.0.0:8080[::]:8080。接著,請從另一台機器確認 curl http://your-vps-ip:8080/ 被拒絕連線。

對於需要對外公開的服務,請執行一個佔用 80 與 443 連接埠並根據 hostname 進行路由的反向代理,且不要發布其他連接埠。這是 Traefik reverse proxy guide 所建立的模式,也是 Nextcloud on a VPS 等自架應用程式僅能透過代理伺服器存取的運作方式。關於如何宣告 ports: 項目以及其餘的 Compose 工作流程,請參閱 the Docker Compose basics guide

當所有內部容器都綁定於 loopback 時,UFW 即可恢復正常功能:守護由 host 本身提供的連接埠。請在此建立規則集,然後依序執行以下指令:

ToolUFW rule generator

實際過濾:DOCKER-USER chain

有時容器連接埠必須對網路公開,但必須受到限制。例如:僅允許特定辦公室位址存取的資料庫複本連接埠。為此,Docker 提供了 DOCKER-USER chain。所有前往容器的封包在經過 Docker 自身的 accept 規則前,都會先經過 DOCKER-USER,且 Docker 永遠不會在該 chain 中寫入規則。此 chain 是供使用者使用的,且 Docker 在 daemon 重啟時不會更動其內容。

執行指令前的一個陷阱:當封包到達 DOCKER-USER 時,DNAT 重寫已經完成。封包的目的地連接埠是容器連接埠(以本範例的 80 為例),而非公開連接埠 (8080)。因此,匹配 --dport 8080 的規則將無法匹配任何內容。可靠的做法是匹配客戶端最初連線的連接埠,這由核心的 connection tracker 記錄:

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP

其含義為:針對從 eth0 進入、且原始目的地連接埠為 8080 的連線封包,丟棄所有非來自 10.0.0.10 的封包。--ctdir ORIGINAL 匹配限制了規則僅作用於「客戶端至容器」的方向,因此不會誤攔截回應封包。請將 eth0 替換為您的公用介面;ip route | grep default 為其命名。使用與先前相同的方式進行測試:從允許的位址執行 curl 會成功,而從其他任何地方連線都會逾時。

使用 iptables 指令新增的規則會在重新開機後消失。由於 UFW 已在管理此防火牆,因此將規則持久化最妥當的位置是 /etc/ufw/after.rules。請在檔案末尾新增以下區塊:

*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMIT

接著執行 sudo ufw reload。UFW 會在每次重新載入與開機時重新套用該檔案,因此您的容器過濾規則現在與防火牆的其他部分位於同一處,並能在重新開機與 Docker 升級後繼續有效。

為何不應停用 Docker 的 iptables 整合

針對此問題的舊有解答建議在 /etc/docker/daemon.json 中設定 { "iptables": false }。請勿執行此操作。Docker 的防火牆規則功能遠不止於發布連接埠 (publish ports)。Masquerade 規則是讓容器能透過主機位址存取外部網路的關鍵;若停用整合,容器將無法下載映像檔 (images)、無法連線至套件鏡像站 (package mirrors),也無法呼叫任何外部 API (application programming interface)。DNAT 規則是讓 -p 運作的基礎,停用後發布的連接埠將完全失效。此外,用於隔離不同 Compose 網路的隔離規則也會消失。若為了規避規則而破壞容器網路,您將必須手動撰寫並維護每一條規則。Docker 官方文件將此設定描述為給「打算手動管理規則」的使用者使用。DOCKER-USER 鏈的存在,正是為了讓使用者不需要開啟此開關。

同樣問題的 IPv6 部分

首先檢查 IPv6 上已發布的 port 狀態:

sudo ss -tlnp | grep 8080

自 Docker Engine 27 起,Docker 預設會管理 ip6tables。在啟用 IPv6 的 Docker network 中,已發布的 port 會在 IPv6 tables 中接受相同的 DNAT 處理,因此存在相同的規避問題,且適用相同的修復方法:DOCKER-USER chain 同樣存在於 ip6tables 中,請使用 sudo ip6tables -I DOCKER-USER ... 鏡像您的規則,並使用 curl 從外部測試伺服器的公用 IPv6 位址,例如 curl -6 http://[2001:db8:2a::1]:8080/

在未啟用 IPv6 的 network 中,IPv6 用戶端改由 docker-proxy 處理。這是一個在 [::]:8080 監聽並透過 IPv4 將流量轉發至 container 的一般使用者空間程序。前往 host 程序的流量會經過 INPUT,因此 UFW 可以過濾該路徑,但前提是 UFW 必須正在管理 IPv6。關於 UFW 是否正在管理 IPv6,以及 VPS 出現 IPv6 漏洞的其他方式,請參閱 UFW 與 IPv6 指南

在 loopback 上發布可以避開此問題:-p 127.0.0.1:8080:80 僅綁定 IPv4 loopback,因此不會產生 IPv6 監聽器,無論在任何 stack 上都無法從外部連線。

支撐架構的模式

  • 將所有內部連接埠皆發布於 127.0.0.1,從源頭避免外部暴露。
  • 將公網端對應至單一反向代理伺服器,由其佔用 ports 80 與 443。
  • 將 UFW 預設設定為 deny,僅允許 SSH 與代理伺服器連接埠。
  • DOCKER-USER 中針對真正的公用容器連接埠進行過濾,比對原始目的地連接埠,並記錄於 /etc/ufw/after.rules
  • 保持 Docker 的 iptables 整合功能開啟。

完成一次設定後,即可避免意外:ufw status 描述主機,DOCKER-USER 描述容器。不會有意外發布的服務,且下次輸入 docker run -p 時,僅會暴露您預期的內容。

FAQ

為什麼 UFW 阻擋了連接埠,我卻仍能連線至 Docker container?

因為 Docker 會在 PREROUTING 鏈中使用 DNAT 規則發布連接埠,這會在進行任何過濾前,將封包的目標地址重寫為 container 的地址。封包接著會走 FORWARD 路徑,而 UFW 的規則位於 INPUT 鏈,封包永遠不會進入該鏈。由於封號未經過防火牆檢查,因此其拒絕規則對已發布的 container 連接埠無效。

如何讓 UFW 阻擋 Docker 發布的連接埠?

UFW 本身無法做到,因為其規則位於錯誤的鏈中。您可以選擇停止暴露連接埠,將其發布為 127.0.0.1:8080:80 以僅允許 host 連線;或者在 DOCKER-USER 鏈中使用 iptables 規則進行過濾,透過 conntrack 匹配原始目標連接埠。請將該規則持久化於 /etc/ufw/after.rules,以確保在重新啟動與 ufw reload 後依然有效。

我應該在 Docker 的 daemon.json 中設定 "iptables": false 嗎?

不應該。該設定會移除所有 Docker 的防火牆與 NAT 規則,這造成的影響遠比繞過防火牆更嚴重。由於 masquerade 規則消失,container 會失去對外網路存取能力;且由於 DNAT 規則消失,已發布的連接埠將無法運作。建議改用 loopback 發布與 DOCKER-USER 鏈;這能在不破壞 container 網路的情況下解決暴露問題。

Docker 在 IPv6 上也會繞過 UFW 嗎?

在 Docker Engine 27 及更高版本中,預設會啟用 ip6tables 管理,因此在支援 IPv6 的 Docker 網路中發布的連接埠,其重寫方式與 IPv4 完全相同(都會繞過 UFW),且需要透過 ip6tables 鏡像相同的 DOCKER-USER 規則。在不支援 IPv6 的網路中,docker-proxy 程序會在 [::] 監聽,該流量會經過 INPUT,若 UFW 有管理 IPv6,則可以進行過濾。在 127.0.0.1 發布則可避免上述兩種情況,因為該處完全沒有 IPv6 監聽。