Docker 绕过防火墙的端口映射问题
问题发现
早上检查服务器时发现他妈的数据库被黑了,排查后才想起防火墙端口忘记关闭。立即登录服务器关闭了防火墙对应端口。
但远程测试时发现:端口居然还能连上!
问题原因

我的数据库服务是通过 Docker 启动的,这正是问题的根源。
Docker 默认把所有转发到容器的流量全部放行,无条件 ACCEPT。
Docker 的 iptables 行为
Docker 默认会在系统 iptables 中自动添加 NAT/FORWARD 规则,将映射端口直接放行。外部流量会绕过 INPUT 链,因此即使防火墙(UFW)没有开放端口,也能访问容器服务。
当 Docker 启动容器并映射端口(如 -p 3307:3306,等价于 -p 0.0.0.0:3307:3306)时,会自动执行以下操作:
- 在
nat/PREROUTING链挂上DOCKER跳转,再由nat/DOCKER链添加 DNAT 规则,把宿主机端口转发到容器 IP 端口 - 在
filter/FORWARD链插入DOCKER-USER和DOCKER跳转,由filter/DOCKER链添加 ACCEPT 规则,放行转发流量
实测一下,一眼就能看出来:
1 | |
注意 FORWARD 链的 policy 可能是 DROP,但第 4 条 DOCKER 跳转里的 ACCEPT 已经把到容器的流量无条件放行了——这正是”防火墙看着是关的,端口却是开的”的根因。
流量路径分析
UFW 规则作用于 filter/INPUT 链,而外部访问 Docker 映射端口时,流量走的是:
1 | |
报文在 PREROUTING 被 DNAT 之后,目的地址已经变成了容器 IP(172.17.0.2),不再是宿主机本机地址。内核一看目的地址不是自己,就判定为「转发」,于是走 FORWARD 链而不是 INPUT 链。
整个 iptables 链的跳转结构长这样,对照着看就清楚了:
由于流量不经过 INPUT 链,所以即使 UFW 禁止了端口,Docker 映射的端口仍然可以被外部访问。
注意 DOCKER-USER 链在 FORWARD 里的位置排在 DOCKER 链之前——这正是它能用来做白名单的原因:你写的 DROP 规则会先于 Docker 自动生成的 ACCEPT 被命中。
为什么绑定 127.0.0.1 就不一样
如果映射写成 -p 127.0.0.1:3307:3306,情况完全不同:
- DNAT 规则带上了
-d 127.0.0.1/32的匹配条件,公网来的报文目的地址是你的公网 IP,匹配不上任何 DNAT 规则 - 匹配不上 DNAT,报文就还是「发给宿主机本机端口」的报文,正常走
INPUT链,UFW 规则生效 - 退一步说,就算 UFW 放行了,
docker-proxy也只在127.0.0.1:3307上监听,公网 IP 上压根没有监听进程,照样连不上
这就是后面方案一为什么是首选的原因——一行参数,零副作用。
解决方案
方案一:端口只绑回环地址(推荐,最小改动)
1 | |
docker-compose 里同理:
1 | |
对外访问统一走本机的反向代理(Nginx / Caddy)转发到 127.0.0.1:3307,代理层再做鉴权和 IP 白名单。容器完全不出现在公网可达面里,这是最干净的做法。
方案二:在 DOCKER-USER 链上做白名单(推荐)
DOCKER-USER 是 Docker 官方预留的自定义链,挂在 FORWARD 链的最前面,优先级高于 Docker 自己写的 ACCEPT 规则,是放自定义规则的唯一正确位置。
只放行内网网段,其余全部丢弃:
1 | |
因为 DNAT 只改目的地址、不改源地址,所以这里看到的 -s 就是真实客户端 IP,按 IP 过滤是可靠的。
有一点必须注意:手工加的 iptables 规则重启后会丢。Docker 重启时不会清空 DOCKER-USER 链,但宿主机重启后规则就没了,需要持久化:
1 | |
或者写个 systemd unit 在 docker.service 之后执行这段 iptables 命令。
方案三:使用 UFW-Docker 插件
不想手写 iptables 的话,用社区脚本让 UFW 能正确管理 Docker 端口:
1 | |
允许特定 IP 访问容器端口:
1 | |
装完记得校验规则是否真的落到了 DOCKER-USER 链里:
1 | |
另外:ufw-docker 和 iptables: false 不能同时用,如果之前改过 daemon.json,要先把 "iptables": false 删掉再重启 Docker。
方案四:关闭 Docker 的 iptables 管理(不推荐)
网上流传最广但坑最深的一个方案:
1 | |
改完 sudo systemctl restart docker,UFW 确实能管住端口了——但代价是:
| 后果 | 说明 |
|---|---|
| 容器出不了网 | Docker 不再写 POSTROUTING 的 MASQUERADE 规则,容器发的包没有源地址转换,直接回不来 |
| 容器间跨网段不通 | DOCKER-ISOLATION-STAGE-* 链不再创建,自定义 bridge 网络之间的隔离和转发全乱 |
| 端口发布变成用户态代理 | 端口实际由 docker-proxy 用户态进程转发,性能下降、连接数受限于进程模型 |
| 要手写全套 NAT | 每次新建容器网络都得自己补 MASQUERADE 和 DNAT |
真要用就得自己把整套 NAT 补齐:
1 | |
结论:这是”用大炮打蚊子”,只有在完全自管 iptables 的场景才考虑。生产环境优先方案一或方案二。
常见坑排查清单
| 问题现象 | 可能原因 | 排查命令 |
|---|---|---|
| 防火墙关了端口还能访问 | Docker 在 nat/DOCKER 写了 DNAT、在 filter/DOCKER 写了 ACCEPT |
iptables -t nat -L DOCKER -niptables -L DOCKER -n |
| UFW 状态显示正常但不生效 | 流量走 FORWARD 链,没经过 UFW 挂的 INPUT 链 | iptables -L FORWARD -n --line-numbers |
DOCKER-USER 里加的规则不生效 |
规则加到了链末尾的 -j RETURN 之后 |
iptables -L DOCKER-USER -n --line-numbers |
| 容器无法访问外网 | 禁用 iptables 后未配置 NAT | iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -j MASQUERADE |
| 容器间无法通信 | 网络模式或 DOCKER-ISOLATION 链问题 |
docker network lsiptables -L DOCKER-ISOLATION-STAGE-1 -n |
| 重启后规则失效 | 手工规则未持久化 | sudo netfilter-persistent save |
| 端口发布只在本机能连 | 正常现象,说明绑定了 127.0.0.1 |
docker port <容器名> |
总结
Docker 默认的 iptables 管理机制虽然方便,但会绕过系统防火墙,带来安全隐患。生产环境建议:
- 敏感服务不要直接映射到公网,端口绑定写成
127.0.0.1:宿主机端口:容器端口,对外访问走反向代理 - 需要放行公网时,用
DOCKER-USER链做源 IP 白名单,并做好规则持久化 iptables: false是最后手段,会破坏容器网络,非必要不用
安全无小事,别等被黑了才想起来补救。