Docker 繞過 UFW 的原因與修正方法
Docker 透過 iptables 規則繞過 UFW,導致 deny 8080 後連接埠仍回應網際網路。了解 PREROUTING 機制,以及使用 DOCKER-USER 或發布設定的有效修正方式。
Docker 為何會繞過 UFW
Docker 會繞過 UFW,因為已發布的容器連接埠不會經過 UFW 管理的防火牆規則。執行 docker run -p 8080:80 時,Docker 會將 DNAT(目的地網路位址轉譯)規則寫入核心的 nat 表中 PREROUTING 鏈。核心決定封包的傳送位置之前,該規則會先將每個封包的目的地改寫為容器的私有位址。改寫後的封包接著會經由 FORWARD 鏈轉送至容器,而該鏈由 Docker 控制。UFW 的規則位於 INPUT 鏈中,封包根本不會進入該鏈。因此,ufw status 顯示預設拒絕,sudo ufw deny 8080 回報成功,而連接埠 8080 仍會回應整個網際網路的連線。
這不是 Docker 的錯誤,UFW 也沒有故障。兩者都是在設定相同的核心防火牆。Docker 的規則只是在封包路徑上較早的位置生效,因此 UFW 根本不會被套用。本指南會示範這項繞過行為,說明其運作機制,接著介紹兩種有效的修正方式:在 127.0.0.1 上發布連接埠,以及在 DOCKER-USER 鏈中進行篩選。如果你不熟悉 UFW,請先參閱UFW 防火牆基礎指南進行設定,因為預設拒絕的防火牆仍是伺服器其他安全設定的正確基礎。
在自己的伺服器上查看繞過情況
先準備一台已啟用 UFW 的 VPS,並將傳入流量的預設政策設為 deny。使用已發布連接埠的方式啟動 Web 容器:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineufw status verbose 顯示 Default: deny (incoming), allow (outgoing),且沒有針對連接埠 8080 的規則。依照防火牆本身的報告,該連接埠處於關閉狀態。現在從另一台機器測試,不要在伺服器本機執行:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OK容器會回應。加入明確的 deny 規則後再次測試:
sudo ufw deny 8080/tcp該連接埠仍會回應,因為 deny 規則位於封包不會經過的 chain 中。UFW 並未失效,而是根本沒有被檢查。這也是問題如此不易察覺的原因:任何地方都不會顯示錯誤,部署可以正常完成,而防火牆狀態輸出看起來完全像是一台已妥善限制存取的正常伺服器。
機制:PREROUTING 先於 INPUT 執行
核心會依固定順序處理傳入封包,整個問題就在這個順序中。
PREROUTING先執行。這裡的規則可以改寫封包的目的地,而 Docker 針對已發布埠的規則正是如此運作。- 接著進行路由決策。目的地是本機的封包會進入
INPUTchain。目的地是其他主機的封包會進入FORWARDchain。 - UFW 的規則位於
INPUT。Docker 的規則位於FORWARD。
查看剛才啟動之容器的 Docker 規則:
sudo iptables -t nat -L DOCKER -nChain 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:80DNAT 這一行就是全部原因。凡是送往 port 8080 的封包,都會將目的地改寫為 172.17.0.2:80,也就是該容器在 Docker 私有 bridge network 上的位址。改寫後,封包的目的地已不是本機,因此路由決策會將它送往 FORWARD 路徑;Docker 已在該路徑加入允許流量進入自有 network 的規則。你的 deny 8080/tcp 規則位於 INPUT,等待一個永遠不會到達的封包。
在 Ubuntu 24.04 上,iptables command 是 nftables 的前端,但 chain 順序與結果完全相同。UFW 與 Docker 都會寫入同一條核心封包處理管線,而 Docker 的進入點更早。這不只適用於 UFW:Rocky 或 AlmaLinux VPS 上的 firewalld 也會在該管線的相同位置進行過濾,並受到相同 DNAT 規則的繞過,因此以下修正方式同樣適用於這些環境。
日常修正:在 127.0.0.1 上發布埠
一開始就不應公開大多數容器。資料庫、反向代理後方的應用程式伺服器、管理面板及 metrics endpoint,都不應直接回應網際網路。請將它們發布在 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 的封包,而來自網際網路的封包不可能合法地帶有該目的地,因此 kernel 會在任何防火牆規則執行前丟棄封包。該埠可從主機存取,其他來源則無法存取。請確認繫結狀態:
sudo ss -tlnp | grep 8080輸出中應看到 127.0.0.1:8080,而不是 0.0.0.0:8080 或 [::]:8080。接著從另一台機器確認 curl http://your-vps-ip:8080/ 會遭拒絕。
需要對外提供服務的項目,請執行一個負責 80 和 443 埠的反向代理,依主機名稱進行路由,且不要發布其他埠。這就是 Traefik 反向代理指南採用的模式,也讓 VPS 上的 Nextcloud 僅能透過代理存取。如何宣告 ports: 項目,以及其餘 Compose 工作流程,請參閱 Docker Compose 基礎指南。
所有內部容器都使用 loopback 後,UFW 就能恢復正常職責:保護主機本身提供服務的埠。請在此建立規則集,然後依序執行下列命令:
實際篩選:DOCKER-USER chain
有時容器連接埠必須維持對網路發布,但仍需限制存取。例如,資料庫複本連接埠可能只允許一個辦公室 IP 位址連線。對此,Docker 提供 DOCKER-USER chain。任何前往容器的封包,都會先通過 DOCKER-USER,再套用 Docker 自己的允許規則;Docker 不會在其中寫入規則。這個 chain 是供你使用的,Docker 也不會在 daemon 重新啟動時修改其中的內容。
執行命令前要注意一個陷阱:封包抵達 DOCKER-USER 時,DNAT 改寫已經完成。封包的目的地連接埠是容器連接埠(本例為 80),不是發布的連接埠(8080)。因此,符合 --dport 8080 的規則不會比對到任何封包。可靠的做法是比對用戶端原先連線的連接埠,因為核心的連線追蹤器會記住該資訊:
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 應成功,從其他位置執行時連線則會逾時。這種等待逾時表示 DROP 規則正在生效,而不是後方沒有服務;遭拒絕的連線與逾時連線之間的差異,是快速判斷連接埠遭到篩選,還是服務根本未監聽的方式。
使用 iptables command 新增的規則會在重新開機後消失。由於 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 的防火牆規則不只是發布連接埠。偽裝規則會讓容器透過主機的位址存取外部網際網路。因此,停用整合後,容器將無法拉取映像檔、連線至套件鏡像站,或呼叫任何外部 API(應用程式介面)。DNAT 規則則是讓 -p 能夠運作的必要條件,因此發布的連接埠會完全失效。隔離不同 Compose network 的規則也會消失。你若要修正這項繞過行為,就會破壞容器網路功能,而且這些規則都必須改由你手動撰寫和維護。Docker 的官方文件指出,這項設定是提供給確實打算自行處理上述工作的使用者。DOCKER-USER chain 的存在,正是為了讓任何人都不需要使用這個開關。
IPv6 端的相同問題
先確認已發布的連接埠在 IPv6 上的狀態:
sudo ss -tlnp | grep 8080自 Docker Engine 27 起,Docker 預設會管理 ip6tables。在啟用 IPv6 的 Docker network 上,已發布的連接埠會在 IPv6 表中套用相同的 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 處理。這是一般的 user-space process,會監聽 [::]:8080,再透過 IPv4 將 network traffic 轉送至 container。連往 host process 的 traffic 確實會經過 INPUT,因此 UFW 可以篩選這條路徑,但前提是 UFW 本身有管理 IPv6。UFW 是否管理 IPv6,以及 VPS 上 IPv6 封鎖缺口的其他成因,請參閱UFW 與 IPv6 指南。
在 loopback 上發布可避開整個問題:-p 127.0.0.1:8080:80 僅繫結 IPv4 loopback,因此不存在 IPv6 listener,外部也無法透過任一網路堆疊連入。
可長期維持的模式
- 將每個內部連接埠發佈到
127.0.0.1,從一開始就不讓它對外暴露。 - 將公開入口交給一個負責 80 和 443 連接埠的反向代理。
- 對主機維持 UFW 預設拒絕,只允許 SSH 和代理連接埠。
- 在
DOCKER-USER中,依原始目的地連接埠比對真正公開的容器連接埠,並將設定持久化到/etc/ufw/after.rules。 - 保持 Docker 的 iptables 整合功能開啟。
設定一次後,就不會再有意外:ufw status描述主機,DOCKER-USER描述容器。任何連接埠都不會意外公開;下一個輸入的 docker run -p 只會公開你明確指定的內容。
FAQ
為什麼 UFW 封鎖連接埠時,我仍然可以連線到 Docker 容器?
因為 Docker 會在 PREROUTING chain 中透過 DNAT 規則發布連接埠。該規則會在任何過濾發生前,將封包的目的地改寫為容器位址。接著封包會沿著 FORWARD 路徑傳遞,而 UFW 規則位於 INPUT,封包不會進入這個 chain。防火牆根本不會檢查該封包,因此其 deny 規則對已發布的容器連接埠沒有作用。
如何讓 UFW 封鎖 Docker 發布的連接埠?
UFW 本身無法做到,因為其規則位於錯誤的 chain。你可以停止公開該連接埠,改以 127.0.0.1:8080:80 發布,讓只有主機可以連線;或是在 DOCKER-USER chain 中使用 iptables 規則,透過 conntrack 比對原始目的地連接埠。將該規則持久化到 /etc/ufw/after.rules,讓它在重新開機和 ufw reload 後仍然存在。
是否應在 Docker 的 daemon.json 中設定 "iptables": false?
不應該。這項設定會移除 Docker 的所有防火牆與 NAT 規則,造成的問題遠不只是繞過 UFW。由於 masquerade 規則消失,容器會失去對外網路存取能力;由於 DNAT 規則消失,已發布的連接埠也會停止運作。請改用 loopback publishing 和 DOCKER-USER chain;這樣可以修正公開暴露問題,而不會破壞容器網路。
Docker 也會繞過 IPv6 上的 UFW 嗎?
在 Docker Engine 27 及後續版本中,ip6tables 管理預設為啟用。因此,在已啟用 IPv6 的 Docker network 上發布的連接埠,會如同 IPv4 一樣繞過 UFW 進行改寫,並需要使用相同的 DOCKER-USER 規則,再以 ip6tables 進行對應設定。在未啟用 IPv6 的 network 上,docker-proxy process 會監聽 [::],該流量會通過 INPUT;如果 UFW 管理 IPv6,UFW 就可以進行過濾。使用 127.0.0.1 發布可避免這兩種情況,因為 IPv6 根本沒有任何 process 監聽。