应用没有问题,隧道也没有问题,可面板就是打不开。当服务以 network_mode: service:vpn 运行时,它会共享 VPN 容器的网络命名空间,这意味着它的监听器和已发布端口属于该命名空间,而不属于服务。由此得出三个决定:应用绑定在哪里、由哪个容器发布端口,以及管理通道有多窄。顺序搞反了,每一个单独设置看起来都正确,组合起来却无法工作。

端口命名空间归谁

在共享命名空间下,给依赖服务再加一条 ports: 条目毫无用处,而且若干网络选项对它本就不受支持。应该改在拥有该命名空间的容器上发布。那个容器也是地址出现在宿主机上的容器,所以映射读起来是一条记录,而不是每个服务一条。

services:
  vpn:
    image: vpn-client-image
    cap_add: [NET_ADMIN]
    ports:
      - "127.0.0.1:8080:8080"
  app:
    network_mode: "service:vpn"

注意映射左侧的地址。绑定到 127.0.0.1 会把已发布端口留在宿主机的回环上,这通常是管理界面想要的。绑定到 0.0.0.0 则会在宿主机的每个接口上暴露,这是另一种安全取舍,不是便利。

让应用在命名空间期望的位置监听

在命名空间内部绑定到 127.0.0.1 的监听器,只能从该命名空间内部访问。已发布的宿主机端口来自外部,所以应用必须绑定更宽的地址,映射才能工作。在大多数服务器上这只是改一行,而人们复制了为无容器部署写好的加固指南之后,跳过的恰恰是这一步。

docker compose exec app ss -lntp
docker port vpn
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/

应用内的监听器列表能说明绑定地址的真实情况,而 docker port 显示归属方实际发布了什么。如果前者显示 127.0.0.1,后者显示宿主机的映射,那你在不碰 VPN 配置的情况下就找到了不一致。

选择管理通道的进入方式

有三种合理方案。在 VPN 容器上发布,从宿主机或局域网访问。在命名空间前面放一个反向代理,只把唯一暴露的端口交给代理。或者让面板只能从命名空间内部通过 exec 会话或 SSH 隧道访问,暴露最小,便利也最小。

哪种合适取决于谁需要这个界面、从哪里访问。同一台宿主机上的单个管理员,回环绑定就够了。需要跨 VPN 访问的团队,需要一条刻意的通道和一个明确的归属方。选代理方案能换来 TLS 和访问日志,代价是多一个需要持续更新的组件。

只开管理端口,别的都不开

VPN 客户端自身的本地访问或锁定策略,可能在出站隧道完全正常时仍然丢弃入站的管理请求。为预期的来源和端口放行一条有记录的例外,然后确认隧道原有的保护没有改变。为了面板能打开而关掉终止开关,是拿真实控制去换一个浏览器标签页。

让应用自身的认证保持开启。身处 VPN 连接的栈内,并不等于授权每个本地用户或每台能到达宿主机的设备,而隧道后面没有认证的管理面板仍然是没认证的管理面板。

从管理设备验证,过一段时间再验证一次

从真正会管理该服务的机器上测试,而不是从宿主机,然后单独验证应用所需的出网路径。管理成功只证明了一个方向,对出站一无所知。

栈重建之后再测试一次。网络归属方被替换会改变应用所挂接的命名空间,而更新前能用的 URL 更新后可能指向不了任何地方。把已发布的映射和绑定地址记下来,这样下次重建是两分钟的检查,而不是重复一遍排查。

相关文章