Docker 与非 Docker 服务的 IP 白名单配置指南
很多人第一次把 Docker、iptables 和 Tailscale 混在一起用时,都会撞上一个很反直觉的坑:明明规则已经写了,公网也确实被拦住了,可某些流量还是能钻进来。
这篇就把底层原因和可直接复用的配置方式讲清楚。目标很简单:只允许指定 IP 访问端口,其他一律拒绝。
Docker 与非 Docker 服务的 IP 白名单配置指南
在真正下规则前,得先搞明白一件事:为什么同样是监听端口,原生服务和 Docker 容器走的防火墙路径不一样。
先搞懂:为什么 Docker 看起来像“绕过了防火墙”
Linux 的 iptables 里,最关键的两条链通常是 INPUT 和 FORWARD。
非 Docker 原生服务
像直接跑在宿主机上的 Nginx、Python、Go 服务,流量最终都会落到 INPUT 链。
也就是说,只要你在 INPUT 链里把端口守住:
- 允许谁进
- 拒绝谁进
规则就会很直白,也很好理解。
Docker 映射端口的服务
如果你是这样启动容器:
docker run -p 8888:80 your-image事情就复杂一点了。
Docker 会接管这部分网络流量。典型情况下会出现两条路径:
- 公网流量:通常经过转发逻辑,主要受
FORWARD链影响。 - 本地或局域网流量:有时会被
docker-proxy接手,直接走INPUT链。
这就是很多人踩坑的根源。
你以为自己只要在 DOCKER-USER 里写一条 DROP 就完了。结果公网确实被拦住了,但从 tailscale0 进来的流量还是能打到容器。
为什么要优先用 DOCKER-USER
Docker 会自己维护一堆规则。如果你直接改它自动生成的链,后面很容易被覆盖。
而 DOCKER-USER 是 Docker 官方专门留出来给你写自定义策略的入口。它的好处很明确:
- 规则位置稳定
- 不容易被 Docker 自动逻辑冲掉
- 更适合做白名单、黑名单、审计和统一拦截
所以只要目标是控制 Docker 映射端口的外部访问,优先从 DOCKER-USER 下手。
场景一:纯公网环境,只允许一个公网 IP 访问
假设服务监听端口是 8888,只允许 203.0.113.5 访问。
方案 A:原生服务
直接改 INPUT 链。
# 先放行指定 IP
sudo iptables -I INPUT 1 -s 203.0.113.5/32 -p tcp --dport 8888 -j ACCEPT
# 再拒绝其他所有访问 8888 的流量
sudo iptables -A INPUT -p tcp --dport 8888 -j DROP这个思路非常朴素:先开白名单,再写兜底拒绝。
方案 B:Docker 服务
这时候推荐写到 DOCKER-USER,并且用 --ctorigdstport 匹配原始目标端口。
# 放行指定 IP
sudo iptables -I DOCKER-USER 1 -s 203.0.113.5/32 -p tcp -m conntrack --ctorigdstport 8888 -j ACCEPT
# 拒绝其他访问 8888 的请求
sudo iptables -A DOCKER-USER -p tcp -m conntrack --ctorigdstport 8888 -j DROP这样能有效拦住大部分直接打到宿主机映射端口的公网流量。
场景二:接入 Tailscale,只允许特定节点访问
假设端口还是 8888,你只想让 100.64.1.2 这台 Tailscale 节点访问。
方案 A:原生服务
原生服务还是老老实实走 INPUT。
sudo iptables -I INPUT 1 -s 100.64.1.2/32 -p tcp --dport 8888 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 8888 -j DROP这里的逻辑很干净:
- 允许指定 Tailscale IP
- 其他所有访问 8888 的流量全部拒绝
方案 B:Docker 服务
这才是最容易翻车的地方。
如果你只在 DOCKER-USER 里封公网,通常还不够。因为某些 Tailscale 流量可能通过 docker-proxy 走进 INPUT。
所以这里要两头一起管。
第一步:在 DOCKER-USER 里封公网入口
sudo iptables -A DOCKER-USER -p tcp -m conntrack --ctorigdstport 8888 -j DROP这一步的目标很明确:让公网对这个端口直接“失明”。
第二步:在 INPUT 里只给指定 Tailscale 节点开口
# 允许指定 Tailscale 节点
sudo iptables -I INPUT 1 -s 100.64.1.2/32 -p tcp --dport 8888 -j ACCEPT
# 拒绝其他从 tailscale0 进入的同端口访问
sudo iptables -I INPUT 2 -i tailscale0 -p tcp --dport 8888 -j DROP这样就形成了一个比较稳的双层策略:
- 公网流量死在
DOCKER-USER - 其他 Tailscale 节点死在
INPUT - 只有白名单里的
100.64.1.2能访问
补充:如果你想允许“所有 Tailscale 设备”访问 Docker 服务
有时候需求没那么细,你只是想:
- 公网全部拒绝
- 只要连进自己 Tailscale 网络的设备都能访问
这时候可以这样写:
# 放行来自 tailscale0 的访问
sudo iptables -I DOCKER-USER 1 -i tailscale0 -p tcp -m conntrack --ctorigdstport 8888 -j ACCEPT
# 拒绝其他访问 8888 的流量
sudo iptables -A DOCKER-USER -p tcp -m conntrack --ctorigdstport 8888 -j DROP这个模式下有个很重要的点:
不要再额外在 INPUT 里给 8888 写一条粗暴的 DROP。
不然你可能会把 docker-proxy 那条本地入口也一起堵死,最后连自己 Tailscale 都访问不了。
规则顺序为什么这么重要
iptables 是按顺序匹配的,先命中先执行。
所以写白名单时,永远记住这个原则:
先放行,再拒绝。
如果你把 DROP 提前写了,后面的 ACCEPT 根本没机会生效。
这也是为什么上面的示例里经常会看到:
-I ... 1插到前面- 或者明确先写允许,再写兜底拒绝
配完以后别忘了持久化
iptables 规则默认在内存里。机器一重启,就可能全没了。
Debian / Ubuntu 常用做法是:
sudo apt-get install iptables-persistent -y
sudo iptables-save | sudo tee /etc/iptables/rules.v4如果你已经装了 netfilter-persistent,也可以直接:
sudo netfilter-persistent save排错时优先看什么
如果你按规则写完还是不通,别急着怀疑人生,先按顺序查这几件事:
- 端口到底是不是宿主机在监听。
- 流量到底走的是
INPUT还是FORWARD。 - 容器是不是经过了
docker-proxy。 - Tailscale 流量是不是从
tailscale0进入。 - 规则顺序有没有把白名单压在
DROP后面。
很多“规则没生效”的问题,本质上不是语法错,而是流量根本没走你以为的那条链。
总结
如果只记一句话,那就是:
- 原生服务优先看
INPUT - Docker 映射端口优先看
DOCKER-USER - 接入 Tailscale 后,Docker 场景要警惕
docker-proxy带来的INPUT旁路
把这三点想明白,绝大多数“明明写了防火墙却还能访问”的问题,都会一下子顺起来。
