Home
avatar

nax

Docker 与非 Docker 服务的 IP 白名单配置指南

很多人第一次把 Docker、iptables 和 Tailscale 混在一起用时,都会撞上一个很反直觉的坑:明明规则已经写了,公网也确实被拦住了,可某些流量还是能钻进来。

这篇就把底层原因和可直接复用的配置方式讲清楚。目标很简单:只允许指定 IP 访问端口,其他一律拒绝。

Docker 与非 Docker 服务的 IP 白名单配置指南

在真正下规则前,得先搞明白一件事:为什么同样是监听端口,原生服务和 Docker 容器走的防火墙路径不一样。

先搞懂:为什么 Docker 看起来像“绕过了防火墙”

Linux 的 iptables 里,最关键的两条链通常是 INPUTFORWARD

非 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

排错时优先看什么

如果你按规则写完还是不通,别急着怀疑人生,先按顺序查这几件事:

  1. 端口到底是不是宿主机在监听。
  2. 流量到底走的是 INPUT 还是 FORWARD
  3. 容器是不是经过了 docker-proxy
  4. Tailscale 流量是不是从 tailscale0 进入。
  5. 规则顺序有没有把白名单压在 DROP 后面。

很多“规则没生效”的问题,本质上不是语法错,而是流量根本没走你以为的那条链

总结

如果只记一句话,那就是:

  • 原生服务优先看 INPUT
  • Docker 映射端口优先看 DOCKER-USER
  • 接入 Tailscale 后,Docker 场景要警惕 docker-proxy 带来的 INPUT 旁路

把这三点想明白,绝大多数“明明写了防火墙却还能访问”的问题,都会一下子顺起来。

Docker iptables Tailscale 防火墙 网络安全