Docker -p 发布端口绕过 ufw 的真实机制与三种收口方案

举报
逍遥渝枫 发表于 2026/10/28 08:33:38 2026/10/28
【摘要】 在云主机上用 ufw deny 关掉了数据库端口,外网却依然能连——问题出在 Docker 发布端口走的是 iptables 的 DNAT 加 FORWARD 链,根本不经过 ufw 管辖的 INPUT 链。本文在一台 Ubuntu 22.04 主机上验证了这条路径,并给出三种收口方案:绑定回环地址走 SSH 隧道、用 DOCKER-USER 链做源头限制、以及 compose 中的内网化改造,每

在一台刚装好 Ubuntu 22.04 的云主机上,很多人会做这样一套「标准操作」:ufw default deny、放行 22/80/443、再补一条 ufw deny 3306 把 MySQL 挡住。看起来天衣无缝。但只要把 MySQL 放进容器并用 -p 3306:3306 发布出去,从外网 telnet 服务器IP 3306 会发现端口照样通着。这不是 ufw 失效,而是 ufw 和 Docker 管的根本不是同一段路。

一、为什么会绕过:先看包走的路径

外部访问一个非容器端口时,包进入 INPUT 链,ufw 的规则链挂在 INPUT 上,ufw deny 3306 生效,路被堵死。但外部访问一个被 Docker 发布的端口时,路径完全不同:包先进入 nat 表的 PREROUTING 链,被 Docker 写入的 DNAT 规则改写目的地址为容器 IP 和容器端口,随后包走的是 FORWARD 链(因为它已经变成「转发」而非「本机接收」),Docker 在 FORWARD 链的 DOCKER-USER 和 DOCKER 链里默认放行了这些端口。而 ufw 管理的 INPUT 链,这条路径压根不经过。

用两条命令可以直接看到证据:

# nat 表 PREROUTING 链里的 DNAT 规则(目的地址已被改写)
sudo iptables -t nat -nL PREROUTING --line-numbers
# FORWARD 链上 Docker 插入的规则,排在 ufw 之前
sudo iptables -nL FORWARD --line-numbers | head -20

输出里会看到 DOCKER 链对每个发布端口有明确的 -j ACCEPT。这就是「ufw deny 了但还通」的全部原因:规则没失效,只是被更靠前的放行规则截胡了。

二、方案一:绑定回环地址,只走 SSH 隧道(推荐首选)

最干净的收口是从根上不让端口面向公网:发布时只绑 127.0.0.1。Docker 的 DNAT 规则会忠实遵守这个绑定,外网流量直接不可达。

# 单条容器命令的写法
docker run -d --name mysql \
  -p 127.0.0.1:3306:3306 \
  -e MYSQL_ROOT_PASSWORD='改成强口令' \
  mysql:8.0

# compose 的写法(bind 地址必须带引号内写全)
services:
  mysql:
    image: mysql:8.0
    ports:
      - "127.0.0.1:3306:3306"

本机其他容器仍可通过容器网络访问它,需要在自己电脑上连时走 SSH 隧道:

ssh -N -L 3306:127.0.0.1:3306 root@你的服务器IP
# 本地用 127.0.0.1:3306 连接,流量经 SSH 加密转发到服务器

验证收口是否生效,可以从另一台外网机器上连一次 3306,应当立即被拒绝;同时在服务器上 ss -tlnp | grep 3306,应看到绑定地址是 127.0.0.1:3306 而不是 0.0.0.0:3306。对数据库、Redis 这类只该给运维和内部服务用的端口,这是首选方案——零额外规则,重启不丢。

三、方案二:DOCKER-USER 链做源头限制

有些端口必须对公网提供服务(比如一个 web 服务),但又想限制来源。Docker 官方为此预留了 DOCKER-USER 链,它在 FORWARD 链上排在 Docker 自己的规则之前,永远不被 Docker 重启时覆盖,是唯一允许人工修改的地方。

# 只放行指定来源 IP 访问 6443,其余全部丢弃
sudo iptables -I DOCKER-USER -p tcp -m conntrack \
  --ctorigdstport 6443 -j DROP
sudo iptables -I DOCKER-USER -p tcp -m conntrack \
  --ctorigdstport 6443 -s 203.0.113.7 -j ACCEPT

# 持久化:DOCKER-USER 的规则不在 docker 的管辖范围,重启会丢
sudo apt install -y iptables-persistent
sudo netfilter-persistent save

这里的 --ctorigdstport 匹配的是 DNAT 之前的原始目的端口。如果直接写 --dport 6443,匹配到的是改写之后的容器端口,只有在主机端口和容器端口一致时才碰巧能用——这是最常见的误用。

方案二的坑也要说清楚:DOCKER-USER 挡的是 FORWARD 方向,容器之间的互访如果也走转发路径会被波及。只针对外网网卡过滤即可:

# 只拦从公网网卡进来的流量,容器互访不受影响(网卡名按实际情况改)
sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 6443 -j DROP

四、方案三:内网化——干脆不发布端口

对于「只有容器自己用」的服务(应用连数据库、任务脚本连 Redis),可以完全去掉 ports 映射,让它们共用一个 compose 内部网络。没有 DNAT 规则,自然没有绕过问题:

services:
  app:
    image: myapp:latest
    networks: [backend]
  mysql:
    image: mysql:8.0
    networks: [backend]
    # 注意:不写 ports,任何形式都不写
networks:
  backend:
    internal: true

internal: true 会把网络做成纯内网,连出网(拉包、发请求)都不允许,适合纯数据层。应用层需要出网的话去掉这个开关即可。

顺带提醒一个反向的坑:网上有人建议在 /etc/docker/daemon.json 里写 "iptables": false 来「让 Docker 别碰 iptables」。不要这么做——Docker 会连 SNAT 伪装规则一起停掉,容器将无法访问外网,且 -p 发布功能整体失效。除非你准备用 ufw-docker 之类的脚本手工重建全部链,否则这条路不通。

五、报错与排查对照表

现象 原因 处理
ufw deny 3306 后外网仍能连容器里的 MySQL 发布端口走 DNAT+FORWARD,不经过 ufw 的 INPUT 链 绑 127.0.0.1 或用 DOCKER-USER 收口
DOCKER-USER 加了规则,重启后失效 DOCKER-USER 不归 Docker 管理,也不会被自动保存 iptables-persistent 固化,或写 systemd oneshot 单元
--dport 匹配的端口「不对」 DOCKER-USER 在 DNAT 之后,匹配到的是容器端口 改用 -m conntrack --ctorigdstport 匹配原始端口
DOCKER-USER 拦截后容器互访也不通了 转发方向全部经过 DOCKER-USER,包括容器间流量 规则上加 -i eth0 只拦公网网卡进入的流量
设置 "iptables": false 后容器无法上网 SNAT 伪装规则被一并停掉 移除该配置;确需防火墙接管用 ufw-docker 脚本重建

六、一条自查清单

每次在主机上新增容器后,建议跑一遍这套动作:docker ps 看发布绑定是不是 127.0.0.1;iptables -t nat -nL PREROUTING 看 DNAT 规则把端口暴露到了哪;从一台真正的外网机器探测一次端口,只看服务器本机的 ss 和 ufw status 是不够的——本机视角看不到 DNAT 之后的转发路径。云安全组、主机 ufw、Docker 发布端口是三层独立的关卡,任何一层以为「下面那层会兜住」的想法,最后都会变成一次凌晨的应急响应。

(本文命令在一台 Ubuntu 22.04 主机上验证,Docker 版本 29.x;其他发行版或更老的 Docker 版本链路一致但链名细节可能不同,请自行确认。)

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。