为什么明明开了防火墙,Docker 容器端口还是暴露了?
前言
刚接触 Docker 或者平常只使用 1Panel 这类面板的小白用户,最常遇到的问题应该就是明明 UFW 里没开放端口,但公网却可以直接通过端口访问容器。
以 1Panel 为例,如果你在应用商店安装应用时,勾选了“端口外部访问”,此时的容器就会监听 0.0.0.0

无论你有没有在 UFW 中开放这个端口,它都已经暴露到了公网。

这个“陈年老坑”的原理很简单,因为 Docker 会直接修改 Linux 的 iptables 规则,而这些规则的优先级高于 UFW,并且在 UFW 中也不会显示 Docker 添加的规则。
要想解决这个问题并不难,下面介绍一下最常见的几种解决方法。
解决方法
禁用 iptables(不推荐)
既然 Docker 会直接修改 iptables 绕过 UFW ,那直接禁用掉 iptables 自动配置,不就轻松秒杀了吗?
在
/etc/docker/daemon.json中添加:{"iptables": false}

相信你看到过网上许多的“庸医”教程就是这么教的,虽然说能够解决端口意外暴露的问题,但禁用 iptables 自动配置的同时还可能导致容器无法访问外部网络或容器间通信失败。
因此非常不推荐直接禁用 iptables。
安全组(推荐)
如果你买的是阿里云、腾讯云、GCP 这些大厂的服务器,这个问题解决起来就非常的简单了,只要在商家后台的安全组中禁用端口,Docker 无论如何也不可能绕过安全组。
但是绝大部分小商家卖的服务器,并没有安全组这类外置的防火墙,只能接着看下面的解决方案,通过合理配置 Docker 和 iptables 来解决这个问题。
监听 127.0.0.1(推荐)
对于大多数用户,端口映射时监听本地 127.0.0.1:8080:80 是最好的解决方法,对应 1Panel 中就是不勾选“端口外部访问”。
-p 127.0.0.1:8080:80ports:
- "127.0.0.1:8080:80"本机的其他应用可以正常通过端口访问到容器,而外部访问不了,如果是 Web 应用可以直接用 Nginx 反向代理。
但这个方法也不是完美的,当你想用另一台服务器反向代理时,监听 127.0.0.1 会导致装有 Nginx 的服务器也访问不到容器。
host 模式
直接使用 host 模式也是一个比较常用的解决方案,在 host 模式下,容器不会获得独立的网络命名空间,而是直接共享宿主机的 IP 和端口,自然就可以用 UFW 来控制端口是否开放。
--networknetwork_mode: "host"但缺点也非常的明显,共享宿主机的网络会导致网络没有隔离、端口冲突等一系列副作用。例如,几个容器的端口都为 80,就没法同时运行了。
虚拟局域网
如果你有用另一台服务器反代的需求,又不希望使用 host 模式,可以用 ZeroTier、Tailscale 之类的组网工具,给两台服务器组一个虚拟局域网,也是个不错的解决方案。
在容器映射端口时,除了映射本机的 127.0.0.0.1:8080:80,再加上一条虚拟局域网的 IP,如 192.168.100.1:8080:80,另一台服务器反代时,填写 192.168.100.1:8080 即可。
-p 127.0.0.1:8080:80 \
-p 192.168.100.1:8080:80ports:
- "127.0.0.1:8080:80" # 仅本机访问
- "192.168.100.1:8080:80" # 仅通过特定局域网 IP 访问ufw-docker(推荐)
要是你既不想用组网工具,又不想(或不能)用 host 模式,还想用另一台服务器的 Nginx 来反代容器,有没有更完美的解决方法呢?
有的,兄弟有的。
ufw-docker 是 GitHub 上开源的一个解决方案,具体原理可以查看仓库的 README,我们这里简单介绍一下如何使用。
手动配置
修改 UFW 的配置文件 /etc/ufw/after.rules,在最后添加上如下规则:
# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:ufw-docker-logging-deny - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j ufw-user-forward
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
-A DOCKER-USER -m conntrack --ctstate INVALID -j DROP
-A DOCKER-USER -i docker0 -o docker0 -j ACCEPT
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -m conntrack --ctstate NEW -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -m conntrack --ctstate NEW -d 172.16.0.0/12
-A DOCKER-USER -j ufw-docker-logging-deny -m conntrack --ctstate NEW -d 192.168.0.0/16
-A DOCKER-USER -j RETURN
-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] "
-A ufw-docker-logging-deny -j DROP
COMMIT
# END UFW AND DOCKER接着重启 UFW systemctl restart ufw。
此时即便你的容器监听的是 0.0.0.0,外部也是无法访问的,同时也不会影响 Docker 容器内部网络以及容器之间的通信。
警告
如果重启 UFW 仍不生效,可能需要重启服务器。
假设现在有一个监听了 0.0.0.0:8080:80 的容器,私有地址为 172.17.0.2,如果要开放端口可以执行以下命令:
警告
port 为内部的 80 端口,并非映射的 8080 端口
ufw route allow proto tcp from any to 172.17.0.2 port 80如果没有填写私有地址 172.17.0.2,则会开放所有内部端口为 80 的容器
ufw route allow proto tcp from any to any port 80如果你有大量容器需要让另一台服务器反代,可以直接对那台服务器的 IP 开放
ufw route allow proto tcp from x.x.x.x to any想要查看或删除规则,可以执行
ufw status numbered删除只需要执行 ufw delete + 编号
ufw delete 2工具
如果觉得以上操作过于繁琐,也没关系,ufw-docker 还提供了工具简化所有操作
wget -O /usr/local/bin/ufw-docker \
https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
chmod +x /usr/local/bin/ufw-docker使用以下命令可以一键修改 UFW 的 after.rules 文件
ufw-docker install具体的操作可以通过 ufw-docker help 查看,这里就不过多介绍了。
ufw-docker allow mysql 3306/tcp
ufw-docker delete allow mysql 3306/tcp
